Skip to content

fix: JSON Data Encoding#4911

Merged
shiroyasha merged 44 commits into
mainfrom
fix/json-data-encoding
May 28, 2026
Merged

fix: JSON Data Encoding#4911
shiroyasha merged 44 commits into
mainfrom
fix/json-data-encoding

Conversation

@andrecalil
Copy link
Copy Markdown
Collaborator

Summary

Fixes canvas event JSON handling so numeric payload values are not silently converted to float64 and later rendered in scientific notation or with precision loss.

Changes:

  • Replaced CanvasEvent.Data JSON storage with a number-preserving models.JSONValue.
  • Decode webhook payloads with json.Decoder.UseNumber().
  • Preserve number tokens when events are emitted, re-emitted, and produced from node executions.
  • Added protobuf serialization handling for json.Number, stringifying only unsafe/exponent-prone values at the Struct boundary.
  • Kept non-number JSON types unchanged: objects, arrays, strings, booleans, and nulls.
  • Added tests for storage scanning, webhook parsing, expression interpolation, and canvas event serialization.

@superplanehq-integration
Copy link
Copy Markdown

👋 Commands for maintainers:

  • /sp start - Start an ephemeral machine (takes ~30s)
  • /sp stop - Stop a running machine (auto-executed on pr close)

@andrecalil andrecalil marked this pull request as ready for review May 20, 2026 12:42
Copy link
Copy Markdown
Contributor

@mchalapuk mchalapuk left a comment

Choose a reason for hiding this comment

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

This review is more about me trying to understand what's going on in the code than anything else.

The PR seems to me like something that would work.

I'm wondering if there's a simpler solution that doesn't involve a special structure for handling JSON in the code and automatically converting the number values to strings.

Wouldn't it be simpler to explicitly convert those strings to numbers when we're creating the JSON structures instead of this special type and auto-convert logic?

Comment thread pkg/grpc/actions/canvases/structpb_compatible.go
Comment thread pkg/grpc/actions/canvases/structpb_compatible.go Outdated
Comment thread pkg/models/json_value.go Outdated
@andrecalil andrecalil requested a review from mchalapuk May 20, 2026 19:47
Comment thread pkg/triggers/webhook/webhook.go Outdated
@superplane-policy-bot superplane-policy-bot Bot requested a review from lucaspin May 21, 2026 20:35
Comment thread pkg/models/json_value.go
Comment thread pkg/triggers/webhook/webhook.go Outdated
andrecalil and others added 19 commits May 21, 2026 19:15
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: André Calil <andre@calil.com.br>
## Summary

- Restore the visual styles from commit 09cbca1 that were dropped
during
  the merge of #4816: shorter (h-10) top and secondary headers, white
secondary background, Tabs-based Canvas/Dashboard toggle, and icon-less
Edit
   button.
- Rewrite CanvasModeToggle to use the shared Tabs primitive (with the
muted
  draft dot) while keeping Dashboard tab support added by #4875.
  - Remove the X close button from the tool sidebar's tab header so the
  Agent/Runs/Versions row matches the original design.
- Always show the Runs and Versions tabs in the tool sidebar — they were
  hidden when editing a version because onToggleVersionControl /
  versionsContent were undefined.

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Pedro Leão <forestileao@users.noreply.github.com>
Signed-off-by: André Calil <andre@calil.com.br>
Summary

  Re-applies the editing-sidebar UX changes from the reverted #4621,
   minus the two behaviors that led to the revert:

  - Skipped: the "show only installed integrations" filter — it hid
  categories during demos when the relevant integration wasn't yet
  connected.
  - Skipped: the icon-rerender flicker when expanding a category —
  block items now stay mounted inside <details> (the browser handles
   show/hide via CSS), and BlockItem is wrapped in React.memo.
  - Removed entirely: the + Integrations button + the
  connect-integrations modal, by request — including all related
  state, props, and the IntegrationGridSection component.

  Sidebar UX

  - Pointer-based resize for both BuildingBlocksSidebar (clamp
  280–640px, default 460) and componentSidebar (clamp 300–800px,
  default 380). Width persisted to localStorage and clamped on read.
  - Categories in BuildingBlocksSidebar: Core → Debugging → Memory
  ordered first, the rest alphabetical. No more filtering by
  configured integrations.
  - CategorySection simplified — drops drag-preview cloning, hover
  state, the GripVerticalIcon, the integration-status Plug
  indicator, and the conditional ItemGroup mount. Icon helper logic
  extracted into resolveCategoryIcon / renderCategoryIcon.

  Canvas append handles

  - Single-channel append (handles.tsx): tightened position (right:
  -15, top: 18, drop transform: none).
  - Multi-channel append (multiRightHandle.tsx): per-channel layout.
   Channel-name text width is measured via canvas.measureText with
  the matching font; all rows align to the widest channel's line so
  the "+" buttons are flush regardless of name length.
  - appendHandle.tsx: new plus-hovered highlight state, horizontal
  nudge so the plus sits inside the connector, hover-driven border
  darken, preview offsets parameterised via CSS vars
  (--sp-append-preview-connector-offset-y,
  --sp-append-preview-offset-y).
  - canvas-reset.css: drops the old hitbox ::before, adds the
  plus-hovered rules to gate the hover styling.
  - RightSideControls spacing gap-1.5 → gap-2.5.

  CanvasPage / workflowv2 plumbing

  - Starter placeholder UX: clicking the floating Add Component
  button when nothing else is open now creates a free-positioned
  placeholder via onPlaceholderAdd({ position }) and opens the
  building-blocks sidebar over it. onPlaceholderAdd's sourceNodeId /
   sourceHandleId are now optional so this path doesn't need an
  edge.
  - handlePlaceholderAdd in workflowv2/index.tsx only adds an edge
  when both source fields are present.

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Pedro Leão <forestileao@users.noreply.github.com>
Signed-off-by: André Calil <andre@calil.com.br>
…#4922)

## Summary
- add example payload fields to integration capability definitions
- serialize example payloads for both legacy and setup-provider
integration capabilities
- map those fields back into `superplane index actions|triggers` and
cover the regression with tests

## Testing
- make pb.gen openapi.spec.gen openapi.client.gen openapi.web.client.gen
- make test PKG_TEST_PACKAGES="./pkg/cli/commands/index
./pkg/grpc/actions/integrations"
- make lint
- make check.build.app

Closes #4912

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: André Calil <andre@calil.com.br>
The rich UI widget syntax reference was missing from agent sessions
after the resource loading refactor. This embeds it as a static Go file
and mounts it at `ref/rich-ui-widgets.md`.

Covers: buttons, surveys, rubrics, charts, mermaid, node/run/integration
chips, draft-actions.

---------

Signed-off-by: Bender Rodriguez <bender@superplanehq.com>
Co-authored-by: Bender Rodriguez <bender@superplanehq.com>
Signed-off-by: André Calil <andre@calil.com.br>
Fixes #4920.

## Summary
- add version_id to workflow_runs and backfill existing runs from the
published version active at run creation
- attach the live canvas version when creating new workflow runs and
expose it through the CanvasRun API
- load the run's recorded canvas version in runs view so
historical/deleted nodes render correctly

## Validation
- make test PKG_TEST_PACKAGES='./pkg/models ./pkg/grpc/actions/canvases'
- make lint
- make check.build.app
- make format.js
- make check.lint.ui
- make check.build.ui
- make check.db.migrations

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: André Calil <andre@calil.com.br>
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Context
Fix requested in PR comment: when using Versions → “Load older versions”
then clicking to view a different version, the already-loaded history
should stay available (no re-loading / re-clicking required).

## What’s in this PR (WIP)
- Adds a frontend regression test for the Versions sidebar to ensure
expanded history remains visible when selecting a different version.

## Next steps in this PR
- Reproduce the issue end-to-end in `workflowv2` and implement the fix
so the test represents the real regression (and add/adjust an
integration-level test if needed).
- Run `make format.js` and `make check.build.ui`.

## Testing
- Unit test added (not yet validated in CI/local in this PR revision).
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-29121969-11c9-4d8a-90ca-1a45f53b71ab"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-29121969-11c9-4d8a-90ca-1a45f53b71ab"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Pedro Leão <forestileao@users.noreply.github.com>
Signed-off-by: André Calil <andre@calil.com.br>
## Summary
- Preserve full markdown bodies for categorized rubric sections instead
of flattening them to criteria only
- Render rubric body markdown with GFM tables, fenced code blocks,
links, blockquotes, and lists
- Keep preserved rubric markdown when starting a build from a rubric
widget

## Tests
- make format.js
- docker compose -f docker-compose.dev.yml exec app bash -c "cd web_src
&& npm test -- --run src/components/AgentSidebar/widgets/parser.test.ts
src/components/AgentSidebar/widgets/RubricWidget.spec.tsx
src/components/AgentSidebar/widgets/RichMessage.spec.tsx"
- make check.build.ui
- make check.lint.ui

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: André Calil <andre@calil.com.br>
fixes #4932

Signed-off-by: Igor Šarčević <igor@operately.com>
Signed-off-by: André Calil <andre@calil.com.br>
## Summary
- clear component sidebar URL state when switching canvas versions or
entering/leaving edit mode
- prevent node run-history loading from the component sidebar while the
canvas is in edit mode
- add a CanvasPage regression test for live-only sidebar event loading

## Validation
- npm run test:run -- src/ui/CanvasPage/index.spec.tsx
- make format.js
- make check.build.ui

Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: Igor Šarčević <igor@operately.com>
Signed-off-by: André Calil <andre@calil.com.br>
Co-authored-by: André Calil <andre@calil.com.br>
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: Igor Šarčević <igor@operately.com>
Signed-off-by: André Calil <andre@calil.com.br>
## Summary
- scope e2e step helpers to subtests so page/session cleanup runs before
the next DB reset
- make manual-trigger setup deterministic through the backend event path
while preserving run-title resolution
- harden canvas node interactions, queued wait cleanup, and local/CI e2e
timeouts

## Validation
- make test.e2e
- make lint && make check.build.app

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: Igor Šarčević <igor@operately.com>
Signed-off-by: André Calil <andre@calil.com.br>
Signed-off-by: André Calil <andre@calil.com.br>
@andrecalil andrecalil force-pushed the fix/json-data-encoding branch from 6709ec3 to edcee23 Compare May 21, 2026 22:16
@andrecalil
Copy link
Copy Markdown
Collaborator Author

@cursoragent update your review

@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented May 22, 2026

Done.

  • Synced to the latest force-updated PR branch.
  • Re-checked the review findings.
  • Fixed the remaining valid issue: oversized JSON integer tokens now stay exact strings in expression data instead of being rounded through float64.
  • Moved JSON integer-token detection into pkg/models and reused it from protobuf conversion.

Pushed commit:

  • 7b2392e07 fix: preserve oversized JSON integers in expressions

Verified:

  • go test ./pkg/models -run 'JSONValue|UnmarshalJSONValue|IsJSONIntegerToken' -count=1
  • go test ./pkg/grpc/actions/canvases -run 'Test__toStructpbCompatible' -count=1
  • go test ./pkg/workers/contexts -run 'Test_NodeConfigurationBuilder_JSONNumber' -count=1
  • git diff --check

Could not run full Docker-backed checks:

  • make lint && make check.build.app fails because docker is not installed in this VM.
Open in Web Open in Cursor 

@andrecalil andrecalil force-pushed the fix/json-data-encoding branch from 7b2392e to 818d0ad Compare May 22, 2026 14:52
Signed-off-by: André Calil <andre@calil.com.br>
Comment thread pkg/workers/contexts/node_configuration_builder.go
Signed-off-by: André Calil <andre@calil.com.br>
EventID: event.ID.String(),
SourceNodeID: event.NodeID,
Input: event.Data.Data(),
Input: map[string]any{event.NodeID: normalizeExpressionValue(event.Data.Data())},
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.

This change is suspicious. Why is this needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To support expressions (hence the sourceNodeID) and to normalize different number types.
What do you find suspicious? Feel free to suggest a different approach.

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.

What do you find suspicious?

I could be misunderstanding this, but to me this looks suspicious to me because all the other changes seem to be about decoding, but for this one, you are actually changing what the ProcessQueueContext.Input value is. @forestileao can you take a look?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is the state here? @lucaspin @andrecalil @forestileao

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I was waiting for Pedro to chime in while I looked at other issues/tasks.

@superplane-policy-bot superplane-policy-bot Bot requested a review from lucaspin May 24, 2026 21:43
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d73581f. Configure here.

return out
default:
return value
}
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.

Duplicate recursive JSON tree-walking logic across packages

Low Severity

toStructpbCompatible in structpb_compatible.go and normalizeExpressionValue/normalizeExpressionMap/normalizeExpressionSlice in node_configuration_builder.go both implement the same recursive tree-walking pattern over map[string]any, []any, and json.Number. The only difference is the leaf conversion strategy for json.Number. Extracting a shared generic walker parameterized by the number-conversion function would reduce the maintenance surface and the risk of one path diverging from the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d73581f. Configure here.

Comment thread pkg/models/json_value.go
if err == nil {
return errors.New("invalid JSON: multiple top-level values")
}
return err
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.

Multi-value detection uses wrong decode target type

Low Severity

UnmarshalJSONValue detects multiple top-level JSON values by attempting a second Decode into &struct{}{}. This works for extra objects or null, but if the trailing value is a number, string, boolean, or array, the decoder returns a type-mismatch error instead of nil. The function still rejects the input, but the returned error message is misleading (a Go unmarshal error instead of "multiple top-level values"). Using new(any) as the target would correctly detect all trailing JSON value types.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d73581f. Configure here.

@shiroyasha shiroyasha merged commit af5ae56 into main May 28, 2026
3 of 4 checks passed
@shiroyasha shiroyasha deleted the fix/json-data-encoding branch May 28, 2026 11:24
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.

7 participants