Skip to content

Explain a drive this device can't reach instead of showing a raw error - #1941

Closed
joepio wants to merge 1 commit into
developfrom
claude/project-thread-u8vgm0-offline-drive-screen
Closed

joepio wants to merge 1 commit into
developfrom
claude/project-thread-u8vgm0-offline-drive-screen

Conversation

@joepio

@joepio joepio commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Before: opening a drive this device holds no copy of, with nothing to fetch it from, showed "Could not open atomic:…" with the raw subject as its title. A red block called it a server problem ("Could not reach the server") above the raw message ("This resource belongs to a local-only drive but was not found in local storage" or "Offline: resource not available locally"). Below that were three competing buttons: Restore from Cloud Vault, Retry and Use proxy.

After: a page titled "This drive isn't on this device yet" explains why, in the style of the peer invite page. When a Cloud Vault backup exists, Restore comes first. Then comes a device diagram (this device, no server or server, your other devices) with a status line that retries by itself every 10 seconds and a Retry now button. Local-only drives get a hint about connecting a server. The raw subject and error are under Technical details, and Use proxy is gone.

It replaces the old page for a whole drive this device lacks: a registered local-only drive, the currently open drive, or any subject when there is no server at all. A missing item inside a local-only drive this device holds was more likely deleted, so that case keeps the old page.

How: new views/DriveUnavailable.tsx, chosen in ErrorPage from isNotAvailableLocally. Retrying calls resumePeerLinks and fetchResourceFromServer. The four locale catalogs gain the new strings, generated by the vite plugin. Rebased on develop as one commit. Screenshots are in the project thread.

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.
@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 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>
@joepio

joepio commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

Landed through batch #1966 (squash 5e189e6, green run 36970563089). Fixes made while batching are listed in #1966.

@joepio joepio closed this Oct 2, 2026
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