Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b2ebbed
Fix: reduce WebChat freezes during drag/resize
marc-romu Dec 22, 2025
57d05ed
chore: update SmartHopperPublicKey with dev public key value
marc-romu Dec 22, 2025
86f9e7d
chore: update bug report template placeholders to reflect current ver…
marc-romu Dec 22, 2025
e1e5acd
Fix: prevent WebChat UI freezes during streaming
marc-romu Dec 22, 2025
aecda72
perf(chat): optimize DOM updates with keyed queue and conditional deb…
marc-romu Dec 23, 2025
0975b9e
perf(chat): optimize message rendering with template caching, LRU dif…
marc-romu Dec 23, 2025
3184521
perf(chat): remove unused stream animation code and fix body filter p…
marc-romu Dec 23, 2025
3a7699b
feat(chat): add instruction_get tool to reduce system prompt size and…
marc-romu Dec 23, 2025
666766c
fix(tools): improve instruction_get reliability and add required para…
marc-romu Dec 23, 2025
4380973
refactor(chat): extract shared streaming logic and optimize provider …
marc-romu Dec 24, 2025
ede9409
docs(changelog)
marc-romu Dec 24, 2025
d335603
refactor(chat): improve error logging and code formatting in WebChat …
marc-romu Dec 24, 2025
c3fc007
perf(chat): improve WebChat stability and streaming performance (#362)
marc-romu Dec 24, 2025
c696584
docs: fix version badge
marc-romu Dec 24, 2025
36d2399
Merge branch 'dev' of https://github.com/architects-toolkit/SmartHopp…
marc-romu Dec 24, 2025
b4f0f9a
perf(component-base): refactored component base for stability
marc-romu Dec 24, 2025
bcbd7ea
perf(providers): add reasoning support for tool calls across all prov…
marc-romu Dec 24, 2025
de01ed3
feat(component-base): new state manager to improve component state st…
marc-romu Dec 25, 2025
02d2365
refactor(components): remove "Done :)" messages and prevent clearing …
marc-romu Dec 25, 2025
32e883b
refactor(component-base): add post-solve guard to prevent premature o…
marc-romu Dec 25, 2025
ad0a5b1
feat(ghjson): add panel color and bounds serialization support
marc-romu Dec 26, 2025
c7e137c
style: remove trailing blank lines and fix copyright inconsistencies
marc-romu Dec 26, 2025
8a67308
fix(header-fixer): strip BOM when comparing and updating file headers
marc-romu Dec 26, 2025
8b9d2f5
style(docs): remove trailing whitespace in GhJSON index
marc-romu Dec 26, 2025
4fb2e11
fix(component-base): improve debounce timer cancellation checks to pr…
marc-romu Dec 27, 2025
174fe9c
refactor: simplify conditional expressions and improve code clarity
marc-romu Dec 27, 2025
a5fc4c7
fix(component-base): stabilize stateful component base with a new Sta…
marc-romu Dec 27, 2025
85cf0cd
chore: update development version date to 1.2.2-dev.251227
actions-user Dec 27, 2025
2822695
chore: update development version date to 1.2.2-dev.251227 (#365)
marc-romu Dec 27, 2025
e5dcf13
chore: prepare release 1.2.2-alpha with version update and code style…
actions-user Dec 27, 2025
216f256
chore: prepare release 1.2.2-alpha with version update and code style…
marc-romu Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Purpose: Ensure SmartHopperPublicKey is anonymized before committing by invoking the PowerShell hook.

set -euo pipefail
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"

pwsh -NoProfile -ExecutionPolicy Bypass -File "$HOOK_DIR/pre-commit.ps1"
43 changes: 43 additions & 0 deletions .githooks/pre-commit.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Purpose: Enforces anonymization of SmartHopperPublicKey before every commit.

$repoRoot = Split-Path -Parent $PSScriptRoot
$anonymizeScript = Join-Path $repoRoot "tools\Anonymize-SmartHopperPublicKey.ps1"
$csprojPath = Join-Path $repoRoot "src\SmartHopper.Infrastructure\SmartHopper.Infrastructure.csproj"
$expectedPlaceholder = "This value is automatically replaced by the build tooling before official builds."

if (-not (Test-Path $anonymizeScript)) {
Write-Error "Anonymize script not found at $anonymizeScript"
exit 1
}

if (-not (Test-Path $csprojPath)) {
Write-Error "Target csproj not found at $csprojPath"
exit 1
}

# Run anonymization to guarantee the placeholder is present.
& $anonymizeScript -CsprojPath $csprojPath
if ($LASTEXITCODE -ne 0) {
Write-Error "Anonymization script failed (exit $LASTEXITCODE)."
exit $LASTEXITCODE
}

# Verify the placeholder was applied to block commits with real keys.
try {
$xml = [xml](Get-Content $csprojPath -Raw)
$keyElement = $xml.SelectSingleNode("//SmartHopperPublicKey")
if (-not $keyElement) {
Write-Error "SmartHopperPublicKey element not found in $csprojPath"
exit 1
}

if ($keyElement.InnerText -ne $expectedPlaceholder) {
Write-Error "SmartHopperPublicKey is not anonymized. Expected placeholder text."
exit 1
}
} catch {
Write-Error "Failed to verify SmartHopperPublicKey placeholder: $_"
exit 1
}

Write-Host "SmartHopperPublicKey anonymized and verified. Proceeding with commit."
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ body:
attributes:
label: Rhino Version
description: What version of Rhino are you using?
placeholder: e.g. RH8.25
placeholder: e.g. RH8.26
validations:
required: true

Expand All @@ -44,7 +44,7 @@ body:
attributes:
label: SmartHopper Version
description: What version of SmartHopper are you using?
placeholder: e.g. 1.2.0-alpha
placeholder: e.g. 1.2.2-alpha
validations:
required: true

Expand Down
12 changes: 6 additions & 6 deletions .github/actions/code-style/header-fixer/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ runs:
)
HEADER_LINE_COUNT=${#HEADER_LINES_ARRAY[@]}

# Compare header per-line ignoring year
# Compare header per-line ignoring year and BOM
# echo "Comparing header for $file"
mismatch=0
mismatch_line=0
for ((i=0; i<HEADER_LINE_COUNT; i++)); do
TEMPLATE_LINE=${HEADER_LINES_ARRAY[i]}
FILE_LINE=$(sed -n "$((i+1))p" "$file")
FILE_LINE=$(sed -n "$((i+1))p" "$file" | sed 's/^\xEF\xBB\xBF//')
TEMPLATE_CAN=$(echo "$TEMPLATE_LINE" | sed -E 's/(Copyright \(C\) )[0-9]{4}/\1####/')
CURRENT_CAN=$(echo "$FILE_LINE" | sed -E 's/(Copyright \(C\) )[0-9]{4}/\1####/')
if [[ "$TEMPLATE_CAN" != "$CURRENT_CAN" ]]; then
Expand All @@ -96,8 +96,8 @@ runs:
echo "Updating header in $file"
echo "DEBUG: mismatch_line=$mismatch_line, HEADER_LINE_COUNT=$HEADER_LINE_COUNT"

# Read file into array for selective updates
readarray -t FILE_CONTENT < "$file"
# Read file into array for selective updates, stripping BOM if present
readarray -t FILE_CONTENT < <(sed '1s/^\xEF\xBB\xBF//' "$file")
# Determine if full header insertion or line update
if [ "$mismatch_line" -eq 1 ]; then
echo "DEBUG: Full header missing, inserting full header"
Expand All @@ -111,8 +111,8 @@ runs:
printf "%s\n" "${FILE_CONTENT[@]}" > "$file"
fi

# Check and fix unbalanced comment blocks in first 20 lines
HEAD_LINES=$(head -n 20 "$file")
# Check and fix unbalanced comment blocks in first 20 lines (strip BOM for analysis)
HEAD_LINES=$(head -n 20 "$file" | sed '1s/^\xEF\xBB\xBF//')
OPEN_COUNT=$(grep -o '/\*' <<<"$HEAD_LINES" | wc -l || true)
CLOSE_COUNT=$(grep -o '\*/' <<<"$HEAD_LINES" | wc -l || true)
echo "DEBUG: OPEN_COUNT=$OPEN_COUNT, CLOSE_COUNT=$CLOSE_COUNT in first 20 lines"
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.2-alpha] - 2025-12-27

### Added

- Chat:
- Added `instruction_get` tool (category: `Instructions`) to provide detailed operational guidance to chat agents on demand.
- Simplified the `CanvasButton` default assistant system prompt to reference instruction tools instead of embedding long tool usage guidelines.

### Changed

- Infrastructure:
- Extracted duplicated streaming processing logic into shared `ProcessStreamingDeltasAsync` helper method in `ConversationSession`, reducing code duplication by ~80 lines.
- Added `GetStreamingAdapter()` to `IAIProvider` interface with caching in `AIProvider` base class, replacing reflection-based adapter discovery.
- Added `CreateStreamingAdapter()` virtual method for providers to override; updated OpenAI, DeepSeek, MistralAI, Anthropic, and OpenRouter providers.
- Added `NormalizeDelta()` method to `IStreamingAdapter` interface for provider-agnostic delta normalization.
- Simplified streaming validation flow in `WebChatDialog.ProcessAIInteraction()` - now always attempts streaming first, letting `ConversationSession` handle validation internally.
- Added `TurnRenderState` and `SegmentState` classes to `WebChatObserver` for encapsulated per-turn state management.
- Reduced idempotency cache size from 1000 to 100 entries to reduce memory footprint.
- Promoted `StatefulComponentBaseV2` to the default stateful base by renaming it to `StatefulComponentBase`.
- Chat UI:
- Optimized DOM updates with a keyed queue, conditional debug logging, and template-cached message rendering with LRU diffing to cut redundant work on large chats.
- Refined streaming visuals by removing unused animations and switching to lighter wipe-in effects, improving responsiveness while messages stream.

### Fixed

- DeepSeek provider:
- Fixed `deepseek-reasoner` model failing with HTTP 400 "Missing reasoning_content field" error during tool calling. The streaming adapter was not propagating `reasoning_content` to `AIInteractionToolCall` objects, causing the field to be missing when the conversation history was re-sent to the API.
- Fixed duplicated reasoning display in UI when tool calls are present. Reasoning now only appears on tool call interactions (where it's needed for the API), not on empty assistant text interactions.

- Chat UI:
- Fixed user messages not appearing in the chat UI. The `ConversationSession.AddInteraction(string)` method was not notifying the observer when user messages were added to the session history.

- Tool calling:
- Improved `instruction_get` tool description to explicitly mention required `topic` argument. Some models (MistralAI, OpenAI) don't always respect JSON Schema `required` fields but do follow description text.

- Chat UI:
- Reduced WebChat dialog UI freezes while dragging/resizing during streaming responses by throttling DOM upserts more aggressively and processing DOM updates in smaller batches.
- Mitigated issue [#261](https://github.com/architects-toolkit/SmartHopper/issues/261) by batching WebView DOM operations (JS rAF/timer queue) and debouncing host-side script injection/drain scheduling.
- Reduced redundant DOM work using idempotency caching and sampled diff checks; added lightweight JS render perf counters and slow-render logging.
- Improved rendering performance using template cloning, capped message HTML length, and a transform/opacity wipe-in animation for streaming updates.
- Further reduced freezes while dragging/resizing by shrinking update batches and eliminating heavy animation paths during active user interaction.

- Context providers:
- Fixed `current-file_selected-count` sometimes returning `0` even when parameters were selected by reading selection on the Rhino UI thread and adding a robust `Attributes.Selected` fallback.
- Added selection breakdown keys: `current-file_selected-component-count`, `current-file_selected-param-count`, and `current-file_selected-objects`.

## [1.2.1-alpha] - 2025-12-07

### Added
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Are you skilled in coding and want to contribute to SmartHopper? You can help fi

- Fork the repository
- Create a new branch referencing the issue you want to fix or feature you want to add
- Configure Git hooks so the anonymization pre-commit runs locally:
```bash
git config core.hooksPath .githooks
```
- Submit a Pull Request following the [Pull Request Guidelines](#pull-request-guidelines) explained below

### 4. **Release Checklist**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SmartHopper - AI-Powered Tools and Assistant for Grasshopper3D

[![Version](https://img.shields.io/badge/version-1.2.1--alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-1.2.2--alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![.NET CI](https://img.shields.io/github/actions/workflow/status/architects-toolkit/SmartHopper/.github/workflows/ci-dotnet-tests.yml?label=tests&logo=dotnet&style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml)
[![Ready to use](https://img.shields.io/badge/ready_to_use-YES-brightgreen?style=for-the-badge)](https://smarthopper.xyz/#installation)
Expand Down
14 changes: 14 additions & 0 deletions SmartHopper.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.OpenR
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Grasshopper.Tests", "src\SmartHopper.Core.Grasshopper.Tests\SmartHopper.Core.Grasshopper.Tests.csproj", "{56E24C95-ADF6-4DBA-BDB7-73CFB1291052}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Tests", "src\SmartHopper.Core.Tests\SmartHopper.Core.Tests.csproj", "{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -196,6 +198,18 @@ Global
{56E24C95-ADF6-4DBA-BDB7-73CFB1291052}.Release|x64.Build.0 = Release|Any CPU
{56E24C95-ADF6-4DBA-BDB7-73CFB1291052}.Release|x86.ActiveCfg = Release|Any CPU
{56E24C95-ADF6-4DBA-BDB7-73CFB1291052}.Release|x86.Build.0 = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|x64.ActiveCfg = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|x64.Build.0 = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|x86.ActiveCfg = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Debug|x86.Build.0 = Debug|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|Any CPU.Build.0 = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x64.ActiveCfg = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x64.Build.0 = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.ActiveCfg = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion Solution.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<SolutionVersion>1.2.1-alpha</SolutionVersion>
<SolutionVersion>1.2.2-alpha</SolutionVersion>
</PropertyGroup>
</Project>
1 change: 0 additions & 1 deletion docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Data flow:

1) Component input/state → 2) Context providers → 3) Provider + Model resolution → 4) AI call (tools optional) → 5) Response decoding → 6) Metrics → 7) Component outputs


## 2. Provider Discovery, Trust, and Loading

- Manager: `src/SmartHopper.Infrastructure/AIProviders/ProviderManager.cs` — docs: [ProviderManager](./Providers/ProviderManager.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/Components/AI/AIModelsComponent.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Provide an up-to-date list of models by querying the provider API when possible
4. Errors (e.g., no provider or no models) emit a runtime error with a concise message.

- Execution trigger:
- `RunOnlyOnInputChanges = false` so provider changes retrigger execution (see `AIStatefulAsyncComponentBase`).
- `RunOnlyOnInputChanges = false` so provider changes retrigger execution.

## Notes

Expand Down
2 changes: 1 addition & 1 deletion docs/Components/ComponentBase/AIProviderComponentBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ Expose provider selection via context menu and store the selection so derived co
## Related

- [AIProviderComponentAttributes](../Helpers/AIProviderComponentAttributes.md) – draws a provider logo/badge on the component.
- [AIStatefulAsyncComponentBase](./StatefulAsyncComponentBase.md) – combines this base with the async state machine.
- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) – combines this base with stateful execution.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Offer a turnkey base to build AI components: choose provider/model, build a requ

## Key features

- Builds on [AIProviderComponentBase](./AIProviderComponentBase.md) and [StatefulAsyncComponentBase](./StatefulAsyncComponentBase.md) to add a `Model` input and a `Metrics` output.
- Builds on [AIProviderComponentBase](./AIProviderComponentBase.md) and [StatefulComponentBase](./StatefulComponentBase.md) to add a `Model` input and a `Metrics` output.
- Capability‑aware model selection via `RequiredCapability` and `UsingAiTools`, delegating to provider `SelectModel()` / `ModelManager.SelectBestModel`.
- `CallAiToolAsync` helper that injects provider/model into AI Tools, executes them, and stores the last `AIReturn` snapshot.
- Centralized metrics output (JSON with provider, model, tokens, completion time, data/iteration counts).
Expand All @@ -24,4 +24,4 @@ Offer a turnkey base to build AI components: choose provider/model, build a requ
## Related

- [AIProviderComponentBase](./AIProviderComponentBase.md) – provider UI/persistence.
- [StatefulAsyncComponentBase](./StatefulAsyncComponentBase.md) – async state machine foundation.
- [StatefulComponentBase](./StatefulComponentBase.md) – stateful execution foundation.
28 changes: 22 additions & 6 deletions docs/Components/ComponentBase/AsyncComponentBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,38 @@ Provide a robust async skeleton: snapshot inputs, run on a background task with

## Key features

- Task lifecycle and cancellation token handling.
- Input snapshotting to avoid race conditions.
- **Task lifecycle** with cancellation token handling and proper cleanup.
- **Worker-based execution** via `CreateWorker()` abstract method.
- **Two-phase solve pattern**: pre-solve (gather inputs, start tasks) and post-solve (set outputs).
- **State tracking** with `_state` counter and `_setData` latch for coordinating async completion.
- **LIFO worker processing**: workers are reversed before output phase for expected ordering.
- Background execution with exception capture and runtime message reporting.
- Hooks for progress updates using [ProgressInfo](../Helpers/ProgressInfo.md).
- Separation of UI thread vs. worker thread responsibilities.

## Key lifecycle flow

1. **BeforeSolveInstance()** – Cancels running tasks, resets async state (if not in output phase).
2. **SolveInstance() [pre-solve]** – `InPreSolve=true`; creates worker, gathers input, starts Task.
3. **AfterSolveInstance()** – Waits for tasks via `Task.WhenAll`, then sets `_state` to worker count and `_setData=1`.
4. **SolveInstance() [post-solve]** – `InPreSolve=false`; calls `SetOutput()` on each worker (LIFO order), decrements `_state`.
5. **OnWorkerCompleted()** – Called when `_state` reaches 0 after output phase.

## Internal state variables

- `_state` – Tracks worker completion count; starts at 0, set to `Workers.Count` when all tasks complete.
- `_setData` – Latch (0/1) indicating output phase is ready.
- `InPreSolve` – Flag distinguishing input-gathering phase from output-setting phase.

## Usage

- Derive your component from [AsyncComponentBase](./AsyncComponentBase.md) when you need async work without a full state machine.
- Implement your execution logic in a background worker (see [AsyncWorkerBase](./AsyncWorkerBase.md)) or the provided async hook.
- Derive your component from `AsyncComponentBase` when you need async work without a full state machine.
- Implement `CreateWorker(Action<string>)` returning an `AsyncWorkerBase`.
- Keep mutable state out of the worker; pass an immutable snapshot of inputs.
- Only access Grasshopper/Rhino UI on the UI thread.
- Use progress callbacks sparingly; throttle if needed.

## Related

- [StatefulAsyncComponentBase](./StatefulAsyncComponentBase.md) – adds state machine, debouncing, and Run handling on top.
- StatefulComponentBase – adds state machine, debouncing, and Run handling on top.
- [AsyncWorkerBase](../Workers/AsyncWorkerBase.md) – worker abstraction to host the actual compute logic.
- [ProgressInfo](../Helpers/ProgressInfo.md) – lightweight progress reporting payload.
Loading
Loading