Skip to content

Canvas: stronger selection, recolor the selection, reliable color taps - #1962

Merged
joepio merged 3 commits into
developfrom
claude/project-thread-i11abh
Oct 1, 2026
Merged

joepio merged 3 commits into
developfrom
claude/project-thread-i11abh

Conversation

@joepio

@joepio joepio commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Requested by Joep · project thread

Related Issues

No issue; requested directly.

Before: a lasso selection was only a thin dashed box with small white corner handles, so it was hard to see what was selected. Picking a color while something was selected only changed the pen, and a tap on the color button sometimes did nothing.

After: every selected stroke, text and image gets a blue glow behind it, the box is solid with a light blue tint, and the corner handles are larger solid blue discs with a white ring. Picking a color from the color button (drag-pick or tap-swap) recolors the selected strokes and text as one undoable step; images keep their look. A tap on the color or size button counts as a tap unless the finger moves more than 10px from where it landed.

How: drawSelectionHalo in canvas-draw.ts paints the glow underneath the existing canvas content (so colors stay true), and drawSelectionOverlay draws the tinted box and bigger handles. recolorSelection in CanvasPage.tsx rewrites the selected elements through commitReplace. The fan gesture now measures drag distance from the press point instead of the button centre.

Checklist

  • Add changelog entry linking to issue, describe API changes
  • Add or update tests if needed (existing canvas-touch.spec.ts lasso test still passes locally)
  • Update docs if needed

joepio commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Main on bb87568: e2e failed on 2 of 4 shards, and both failures are outside the canvas.

  • table-refresh.spec.ts "reloading a table does not add empty rows": editable-title was not visible within 30s after a reload. It does not touch the canvas.
  • identity-bring-along.spec.ts "a guest workspace comes along when signing in to an existing account": a known flake of about 2 in 10 runs (a guest workspace can be abandoned on sign-in).

The canvas specs, the unit stage and the downstream saas build passed. I'm re-dispatching Main once to confirm.


Generated by Claude Code

@joepio
joepio marked this pull request as ready for review October 1, 2026 17:01
@joepio joepio changed the title Canvas: make the selection stand out Canvas: stronger selection, recolor the selection, reliable color taps Oct 1, 2026
@joepio

joepio commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Trekmeester: included in batch #1966 (run 36898843808). It lands when that run is green; if this PR breaks something there, I take it out and say why here.

@joepio
joepio merged commit 3ac520d into develop Oct 1, 2026
12 of 14 checks passed
@joepio
joepio deleted the claude/project-thread-i11abh branch October 1, 2026 18:23
joepio added a commit that referenced this pull request Oct 2, 2026
joepio added a commit that referenced this pull request Oct 2, 2026
- CanvasPage: the develop merge declared FAN_DRAG_THRESHOLD twice (#1962 landed
  on develop and in this batch); keep it once, each constant with its comment.
- #1960: usageMetadata/compactionTokens as a pure module with tests, including
  that the last tool step's input wins once the SDK merges every part.
- #1523: rowIsCommentable, tested with a real deferred-genesis draft from the
  store (final subject, still new) and the legacy _new: key.
- #1941: DriveUnavailable test: the 10 s retry and Retry now must not leak a
  rejection (fails with an unhandled rejection without the fix).
joepio added a commit that referenced this pull request Oct 2, 2026
…ts, AI fixes, presence order, vault quiet, canvas selection (12 PRs) (#1966)

* Comment on a table row from its gutter

A table row is a resource of its own, so its comment thread is already the
ordinary one — Messages whose `about` points at the row. What was missing was
a way to reach it: the Comments panel could only ever be about the page's own
resource, and a row is never that.

Two pieces:

- The grid takes a `RowHeaderAddonComponent`, rendered in each row's gutter
  left of the row number. It knows rows only by index, so the table page
  supplies the index -> subject mapping (`getRowSubject`) and the component.
  The gutter is widened to hold a bubble beside the row number, and row-scoped
  affordances marked `data-row-affordance` are revealed by hovering anywhere in
  the row rather than only over the gutter.
- The panel gains a target. `commentSubject` says which resource the thread is
  about, `undefined` meaning the page's own — what the NavBar button opens.
  Aiming it at another resource switches threads instead of toggling, so
  clicking row 3's bubble while row 1's thread is up does what it looks like.
  A close request carries no target and so cannot be expressed as a toggle:
  `closePanelState` closes the panel whatever it is aimed at, which is what a
  deleted target, a navigation and the drawer's own dismiss all need.

The bubble stays put, with its count, for a row that has comments (highlighted
while some are unseen) and appears on hover for one that has none.

Not covered: reaching the bubble by keyboard. It shares the gutter's
mouse-only reachability with the existing expand button, because the grid owns
Tab for cell navigation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooZ4RQshFY8YncRNq6Qpe

* Resolve a row's subject the way the grid renders it

The gutter bubble never appeared for the rows it most needed to: the ones you
had just typed. It asked the collection for every row's subject and treated
`index >= memberCount` as "no resource yet", which is not what that boundary
means. A row added this session keeps its local `_new:` key for its whole life
on the page — materializing persists it but deliberately does NOT turn it into
a collection member, so react-window never remounts it mid-edit. Past the
member count sat real, saved rows, reported as having nothing to point at.

So the context now reports which of the two kinds a row is (`rowSource`),
mirroring what `TablePage/TableRow.tsx` itself renders, and the bubble uses the
matching hook for each: `useMemberFromCollection` for a member,
`useResource` on the `_new:` key for a session row, which the store aliases to
the real subject once it materializes. The two cases genuinely need different
hooks — a session row has no subject at all until then — which is why this
reports the source rather than resolving to a subject.

Caught by `table-row-comments.spec.ts`, which now passes end to end against a
real server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooZ4RQshFY8YncRNq6Qpe

* Add a view when choosing a type from a tab, never replace the last one

Choosing Calendar from the menu of a table's only view tab turned that
view into a calendar, and the table layout was gone (#1806). The user
should always be able to switch between all the applicable views.

- A tab's menu now has an "Add a view" section (built-in kinds and
  apps), the same as the + tab: it adds a view named after the type and
  switches to it, leaving the tab's view intact.
- "Change this view to" is offered only while another view of the
  current type remains, so the last table view (or the only view of an
  app) can never be converted away. setViewKind enforces the same rule
  and adds a view instead.
- Delete is disabled on the last table view while other views exist.
- A converted view whose name was a default ("Default View", a type
  label, a template view name, or an "All <rows>" table name) takes the
  new type's name; a name a person chose is kept.

Closes #1806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Ask the drive for its apps again when a view menu opens (#1846)

useDriveApps published nothing until its whole mount chain had finished:
findSchema, the local app query, then an awaited getResource per app, one
after the other. Live events that landed meanwhile were held back too. After
a reload, an app whose sync delta could not apply to the page's copy
("incomplete Loro import ... missing base state") sits as a placeholder under
repair, and getResource waits up to 10 s on it, which is as long as the
money e2e waits for the "New app" menu item.

- Attach the listeners before anything else and fold every answer in as it
  arrives, reading the live resource each time, so no answer overrules a
  newer one and no slow read holds back the others.
- Read the query's members in parallel.
- Expose refresh(), and call it when "+ Add view" and a view tab's menu
  open, so opening a menu re-runs the query and re-reads what it knows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add a SessionStart hook for Claude Code cloud sessions

Sets up a fresh Claude Code on the web container so agents can lint, test
and run the stack straight away: the pinned Rust toolchain and WASM targets,
wasm-pack, wasm-opt, pnpm install, the @Tomic package builds, Playwright's
browsers for @tomic/e2e and a warm debug build of atomic-server (which also
builds data-browser and the WASM).

Two sandbox workarounds:
- The proxy blocks cdn.playwright.dev, so the Chrome for Testing builds
  come from the storage.googleapis.com mirror.
- Containers have no IPv6, so ATOMIC_IP=0.0.0.0 is exported for the
  session; atomic-server binds to :: by default and fails otherwise.

Only runs when CLAUDE_CODE_REMOTE=true. .gitignore now tracks the shared
hook and settings while keeping local .claude state ignored, and AGENTS.md
says to keep the hook in sync with the environment requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168N2XS6KXcewxhP3XTwDa6

* Say where the time went when the demo setup stalls

The 45 second stall notice is a fixed deadline, so a machine that is slow and
one that is stuck report the same thing. Sentry ATOMIC-BROWSER-1J holds twelve
of them across four releases, spread over every step (identity 7, workspace 3,
storage 2, and one that never left the sign-in check), from Chrome on Windows
and Mac alike, and nothing in them says which kind it was. Setup measures 17
to 28 seconds on a desktop CPU, so 45 seconds is under twice the ordinary
time, and a slow laptop could reach it while still making progress.

The report now carries how long each step took and whether the tab was in the
background, which browsers throttle. And a run that does finish after the
notice sends a second, informational report with its total time and its
slowest step, so a stall that ends is a slow machine, and one with no such
follow-up is a hang.

That is all this changes: what to fix depends on which of the two the reports
turn out to be, and twelve events do not say.

* Name the failing request in the account passkey error

The message was the same for the status check, both start calls and both finish calls, and dropped the HTTP status. It now ends with the request and status, for example (/use/finish: 401).

* Build in the background and use the environment cache in cloud sessions

Only a cloud environment's setup script is cached between sessions; hooks
run every session on a fresh clone. The previous hook redid the whole cold
setup, server build included (10-20 min), while blocking the session.

Split it in three:
- cloud-setup.sh installs toolchains, node_modules, Playwright browsers and
  crates, skipping what is present. It fits the setup script's five-minute
  cache limit (about a minute from cold), so AGENTS.md suggests calling it
  from there; the hook also runs it, so sessions work without that.
- cloud-build.sh builds the @Tomic packages and the debug atomic-server,
  writing packages.done / server.done markers under /tmp/atomic-setup.
- session-start.sh exports ATOMIC_IP, runs cloud-setup.sh, starts
  cloud-build.sh in the background and tells the agent, through
  additionalContext, how to wait for it.

With a cached environment the hook now blocks for about a minute (mostly
linking node_modules) instead of the full build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168N2XS6KXcewxhP3XTwDa6

* Don't report a vault backup as failed when another tab switched account

A second tab signing in as a different account takes over the shared portal
cookie. A backup pass the first tab already had in flight then reaches the
control plane as that account and is told the enrollment does not exist,
which was logged as a failed backup. When the signed-in account no longer
matches the one the pass started for, it is now skipped quietly, like the
401 case.

* Explain a drive this device can't reach instead of showing a raw error

A drive with no copy here and nothing to fetch it from (a local-only drive
while no other device is open, or no server at all) now gets its own page:
what is going on, the Cloud Vault restore first when a backup exists, a
device diagram, automatic retries every 10 seconds, and the raw error under
Technical details. The open drive counts as a drive even before this device
registered it as local-only.

* AI chat: compact on last-step context size, not summed multi-step usage

* Canvas: make the selection stand out with a glow, tinted box and bigger handles

* Canvas: picking a color recolors the selected strokes and text

* Canvas: measure a color or size tap from where the press landed

* OpenRouter return: open the AI sidebar, and don't error on a code-less return link

* Send PRESENCE_SUBSCRIBE before the presence update that follows it

subscribePresence() is deferred behind authenticate().then(), while the
presence manager sends its first update (and reSubscribeAll() its
rebroadcast) synchronously. The update reached the server ahead of the
subscribe frame and was dropped as 'presence update from non-subscriber'.

Hold presence updates for a drive while its subscribe is pending and send
them right behind it. Nothing is held before authentication, and a
withdrawn subscription drops what was held, so the server's subscribe-time
read check stays the only gate.

* Batch fixes: DriveUnavailable catches its expected fetch failures (#1941); HANDLE_HIT_RADIUS gets its doc comment back (#1962)

* Settle translation catalogs for the batch

* kanban/timer specs: convert a copy, since #1902 keeps the only view of a type from being converted

* CI traces: name the Dagger function failedTestResults, which the CLI can call (e2eTestResults was 'unknown command')

* @tomic/lib unit tests: 20 s timeout, since a loaded CI host timed out ten millisecond tests at 5 s

* Batch fixes and coverage

- CanvasPage: the develop merge declared FAN_DRAG_THRESHOLD twice (#1962 landed
  on develop and in this batch); keep it once, each constant with its comment.
- #1960: usageMetadata/compactionTokens as a pure module with tests, including
  that the last tool step's input wins once the SDK merges every part.
- #1523: rowIsCommentable, tested with a real deferred-genesis draft from the
  store (final subject, still new) and the legacy _new: key.
- #1941: DriveUnavailable test: the 10 s retry and Retry now must not leak a
  rejection (fails with an unhandled rejection without the fix).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Michiel de Jong <michielbdejong@ontola.io>
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.

1 participant