Skip to content

CLUE-620: make the sketch tile a highlight target - #2956

Open
kswenson wants to merge 4 commits into
CLUE-603-linked-representation-referencesfrom
CLUE-620-highlight-drawing-tile
Open

CLUE-620: make the sketch tile a highlight target#2956
kswenson wants to merge 4 commits into
CLUE-603-linked-representation-referencesfrom
CLUE-620-highlight-drawing-tile

Conversation

@kswenson

@kswenson kswenson commented Aug 12, 2026

Copy link
Copy Markdown
Member

Makes the Sketch tile a highlight target. Hovering a variable chip in a Text tile now lights up the sketch's chip for that variable, alongside the Dataflow nodes CLUE-603 already highlighted.

Jira: CLUE-620

Based on #2953 (CLUE-603) and targeted at that branch, so the diff here is only the CLUE-620 work. Retarget to master once #2953 merges.

Why this turned out cheap

The CLUE-603 design deferred this increment behind CLUE-598, reasoning that "nothing can point at a sketch object until the AI can emit object references." That holds for rectangles and freehand strokes — but not for the drawing tile's variable chips. VariableChipObject stores a variableId outright, so CLUE-603's text chip already points at them. No new source, no CLUE-598 dependency, and no changes to the core highlight machinery.

The whole tile-side contribution is six lines:

getObjectsForVariable(variableId: string): IClueTileObject[] {
  return Object.values(self.objectMap)
    .filter(o => o?.type === "variable" && (o as any).variableId === variableId)
    .map(o => ({ objectId: o!.id, objectType: o!.type }));
}

It also makes a better reference implementation than Dataflow, which matches a derived string ("SIM" + variable.name) and therefore loses the association when a variable is renamed. The drawing tile stores the id and has no such flaw.

Notable decisions

The read-only gate is untouched. drawing-layer.tsx gates all highlighting off with if (!this.props.readOnly). That gate belongs to the editing affordances — canvas hover, object-list row hover — which are meaningless when you cannot edit. A highlight directs attention and must show in read-only documents, 4-up cells and thumbnails, as text chips and Dataflow nodes already do. So the highlight renders as a sibling outside the gate rather than changing it, and highlightObject keeps its string | null contract. No existing behavior moves.

renderSelectionBorders is deliberately not reused. It reads object.boundingBox, the object's box in its own coordinate space. An object inside a GroupObject renders within the group's scale() transform, so a layer-level ring drawn from the raw box lands in the wrong place. The highlight path uses getObjectBoundingBox, which walks enclosing groups. Same reason the render enumerates objectMap rather than objects: objects is top-level only, so a chip nested in a group would resolve as a target and never draw.

Ring colors are shared, not copied. src/components/highlight-vars.scss now holds $highlight-preview-ring and $highlight-pinned-ring, consumed by both the sketch ring and the Dataflow node rings, so one reference reads the same way wherever it lands. Only geometry stays local — stroke width and dash length divide by the current zoom, which CSS cannot do. The accessibility rationale for the preview color (3.0:1 against a white canvas, per WCAG 1.4.11) moved into that file with the values.

Testing

  • UnitgetObjectsForVariable (matching, multiple chips, non-variable objects, no match); collectHighlightedObjects; plus composition guards asserting the tile is reachable from a document-level variable reference at all, which depends on drawing-content routing through tileContentAPIViews.
  • Cypresshighlight_references_spec.js gains a test that hover previews and click pins the sketch chip. The fixture's second chip (Gripper) is load-bearing: the feature exists to direct attention to one thing, so asserting the Gripper chip stays dark is what proves the highlight discriminates rather than lighting every variable chip.
  • Full suite green: 3644 unit tests, check:types clean, lint:build clean.

CI

The regression run has one failure: document_tests/tiles_copy_test_spec.js → "Verifies copy button states based on tile selection", asserting .tile-row has length 32 and finding 22.

Known flaky and unrelated to this PR. It has been failing the same way on other PRs. Please disregard it when reviewing.

The second commit, and why it is here

The cypress spec passed while the feature did nothing on screen. Two independent harness problems, both of which made the test agree with the runner rather than with reality:

  1. The doc-editor restores a document from sessionStorage, then replaces that model once document= finishes loading. Lazily-registered tile types can stay bound to the superseded instance while eagerly-present tiles move to the new one — two document-content instances in one pane. Highlight refs are volatile and per-document, so they cannot travel between them. Cypress starts with clean session storage and never hits it; a browser tab open for a while hits it immediately.
  2. The route renders up to three copies of the document, and the read-only remote copy is rebuilt from a snapshot on every change — continuous, for a fixture with a running Simulator. Unscoped selectors were satisfied by a ring in any copy.

The spec now pins noStorage=true and disables both read-only copies, so every assertion refers to the one document a person interacts with. That removed the session storage the chip-deletion test had been leaning on between tests; it now selects the paragraph with a triple-click rather than Cmd+A, whose scope depends on whether focus landed on the tile or inside Slate — selecting nothing in one case and every tile in the document in the other.

CLAUDE.md claimed the three panes share one content model. They do not; corrected, with the diagnosis recorded so nobody re-derives it.

Bugs filed along the way

Neither is a regression from this change, and neither blocks it.

  • CLUE-625 — grouping a variable chip in the Sketch tile throws. VariableChipObject extends DrawingObject rather than SizedObject and never implements setUnrotatedDragBounds, which createGroup calls. Reachable from the Group toolbar button, which does not filter variable chips out of the selection.
  • CLUE-626 — the doc editor binds lazily-registered tiles to a superseded document instance (item 1 above). Worth attention beyond testing: a tile on the orphaned instance is mutating a document that is no longer the one being saved.

Reviewer notes

  • docs/highlights.md is updated with the sketch tile as the second target implementation, and reframes the variable-only restriction as a source limitation — the object kind is fully resolved but has no producer, because a variable chip is the only way a user can currently author a cross-tile reference. Expect it to dissolve when AI-emitted references land, rather than be fixed.

  • Try it on the branch deploy — hover the EMG chip in the text tile. The
    sketch's EMG chip and the Dataflow Sensor node should ring together, and the sketch's Gripper
    chip should stay dark; that last part is the point of the feature, not a detail. Click to pin,
    click again to unpin.

    The noStorage=true in that link is required rather than decorative — without it you may hit
    CLUE-626 and watch the sketch do nothing, which is the dead end this PR's second commit exists
    to prevent. (Relative unit= and document= params resolve against the branch root via
    getAssetUrl, so they work on the deploy as-is.)

🤖 Generated with Claude Code

kswenson and others added 2 commits August 12, 2026 16:04
Drawing objects can be variable-bound: VariableChipObject stores a variableId
outright, so the text tile's variable chip already points at them. Implementing
getObjectsForVariable is all that was needed to reach them -- no new source, and
no changes to the core highlight machinery. That also makes this a better
reference implementation than Dataflow, which matches a derived string and so
loses the association when a variable is renamed.

The ring is drawn at layer level from getObjectBoundingBox rather than reusing
renderSelectionBorders, which reads the object's own untransformed box and would
misplace a ring on a group member. It renders outside drawing-layer's readOnly
gate: that gate belongs to the editing affordances, while a highlight directs
attention and must show in read-only documents, 4-up cells and thumbnails.

Ring colors move to components/highlight-vars.scss, shared with the Dataflow
node rings so one reference reads the same way wherever it lands.

The demo fixture gains a Sketch tile with EMG and Gripper chips; asserting the
Gripper chip stays dark is what proves the highlight discriminates rather than
lighting every variable chip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The highlight spec passed while the feature did nothing on screen, for two
independent reasons, both of which made the test agree with the runner rather
than with reality.

The doc-editor restores a document from sessionStorage and builds a model from
it, then replaces that model once the document= param finishes loading. Tile
types that register lazily can stay bound to the superseded instance while the
eagerly-present tiles move to the new one, leaving two document-content
instances in a single pane. Highlight refs are volatile and per-document, so
they cannot travel between them. Cypress starts with clean session storage and
so never hits this; a browser tab open for a while hits it immediately. Pinning
noStorage makes the spec depend on a stated condition. Filed as CLUE-626.

It also renders up to three copies of the document, and the read-only remote
copy is rebuilt from a snapshot on every change -- continuous, for a fixture
with a running Simulator. Unscoped selectors were therefore satisfied by a ring
in any copy. Disabling both read-only copies makes every assertion refer to the
one document a person interacts with.

Turning those off removed the sessionStorage the chip-deletion test had been
leaning on between tests. It now selects the paragraph with a triple-click
rather than Cmd+A, whose scope depends on whether focus landed on the tile or
inside Slate: it selects nothing in one case and every tile in the document in
the other.

CLAUDE.md said the three panes share one content model. They do not.

CLAUDE.md also picks up three testing notes learned while working CLUE-603:
dispatching a single spec via the Manual Regression workflow, the /editor/
route's multi-pane rendering, and how branch preview URLs drop the ticket
prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.14%. Comparing base (404b0dc) to head (69483a9).

Additional details and impacted files
@@                              Coverage Diff                              @@
##           CLUE-603-linked-representation-references    #2956      +/-   ##
=============================================================================
- Coverage                                      86.14%   86.14%   -0.01%     
=============================================================================
  Files                                            982      982              
  Lines                                          56076    56117      +41     
  Branches                                       14791    14802      +11     
=============================================================================
+ Hits                                           48306    48341      +35     
- Misses                                          7750     7756       +6     
  Partials                                          20       20              
Flag Coverage Δ
cypress-regression 71.33% <100.00%> (-0.02%) ⬇️
cypress-smoke 41.39% <31.57%> (+0.09%) ⬆️
jest 56.94% <58.53%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress

cypress Bot commented Aug 12, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19801

Run Properties:  status check passed Passed #19801  •  git commit 69483a931a: CLUE-620: correct what the doc-editor panes actually share
Project collaborative-learning
Branch Review CLUE-620-highlight-drawing-tile
Run status status check passed Passed #19801
Run duration 03m 07s
Commit git commit 69483a931a: CLUE-620: correct what the doc-editor panes actually share
Committer Kirk Swenson
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

Copilot AI 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.

Pull request overview

Adds Sketch tile variable chips as highlight targets alongside Dataflow nodes.

Changes:

  • Resolves and renders highlighted Sketch variable chips.
  • Shares highlight colors across target tiles.
  • Adds unit/Cypress coverage and improves /editor/ test isolation guidance.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/public/demo/docs/emg-highlight-demo.json Adds the Sketch highlight fixture.
src/plugins/shared-variables/drawing/variable-highlight.test.ts Tests variable target resolution.
src/plugins/drawing/model/drawing-content.ts Exposes matching variable chips.
src/plugins/drawing/components/drawing-tile.scss Styles Sketch highlight rings.
src/plugins/drawing/components/drawing-layer.tsx Renders highlight borders.
src/plugins/drawing/components/drawing-layer-highlight.test.ts Tests highlight collection.
src/plugins/dataflow/nodes/node-states.scss Uses shared highlight colors.
src/components/highlight-vars.scss Defines shared ring colors.
docs/highlights.md Documents Sketch targeting.
cypress/e2e/functional/tile_tests/highlight_references_spec.js Adds end-to-end Sketch coverage.
CLAUDE.md Documents Cypress/editor behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/plugins/drawing/components/drawing-layer.tsx
Comment thread CLAUDE.md Outdated
conditionallyRenderObject omits an object when it is neither selected nor
visible, so hiding a variable chip from the object list panel and then hovering
the text chip drew a ring around empty space. docs/highlights.md already claimed
hidden objects simply show nothing; this makes that true.

The rule is deliberately not just `visible`: a hidden object that is selected
still renders, and should still be ringed.

Also scopes the CLAUDE.md warning about cross-tile state on the doc-editor
route. It said such behavior cannot be verified there at all, which was written
before the sessionStorage cause was found and contradicted the noStorage
guidance a few lines above -- steering readers away from the setup this spec
actually uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The earlier note credited the panes with splitting tiles across document
instances. They do not. The editable pane and the Read Only Local copy share one
document; only the Read Only Remote copy is separate, and deliberately so --
it is rebuilt from a snapshot to emulate a remote client.

The extra instances that instrumentation found came from the sessionStorage
restore-then-replace, which is the thing that actually breaks cross-tile
ephemeral state. Verified after the fact: the branch deploy shows all three
panes and the feature works there, because the URL passes noStorage=true.

So noStorage is what matters for correctness, and disabling the read-only copies
only makes unscoped assertions refer to the pane a person is looking at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants