Repository navigation
Add a version manager for linking workspaces and moving versions - #63
Merged
Merged
Conversation
Links Onshape workspaces to each other and moves external references between them: a pull updates this workspace's references onto the newest versions of what it consumes, and a push cuts a version here and moves the references of the workspaces that consume it onto it. Rebuilt from the implementation this repo carried before the rewrite (5fb1ff5, Jan 2024), with three changes: - Links live in the app's own D1 database, one row per edge, rather than in a Firestore collection holding both ends separately, where a link could half-exist when only one of the two writes landed. - A push updates the workspaces linked directly downstream and stops. The old one walked the whole chain, cutting a version in every document along it; that is now the opt-in recursive push, which needs those versions for the next hop to reference. - Both run in a Workflow, so a chain of Onshape writes caught by a rate limit backs off and resumes rather than leaving a version cut and half the references moved. The page is a tab beside the libraries, shown only when the panel was opened in a workspace, since that is what it acts on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Each linked workspace gets its own button: an upstream row pulls that workspace's latest version, a downstream row pushes to that workspace alone. Both narrow what the run touches rather than changing what it does, so the whole-graph buttons above them are unchanged. A row's pull runs on the click, having nothing to ask for. A row's push opens the version form first — a version cannot be cut without a name — with the recursive option left out, since a push aimed at one workspace has nowhere further to go. The scope each route takes is now a tagged shape instead of a boolean and a string, so "one linked workspace" is sayable in both directions and an invalid combination is not. The named workspace is checked against the links before anything runs. Also: the add-link field is a card with the field and an Add button on one row, as the app this came from had it, and the recursive option says "Recursive push". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Every button runs its push or pull straight away. A push with no name takes the local date and time, which is what somebody wanting their change visible downstream is after; the caret beside each button opens the form that names one, seeded with that same default so editing starts from something. The page-level buttons are gone. Each group's action lives in its title — pull in Parents, push in Children — and each row carries its own, so there is nothing left that acts on "everything" from outside a group. Recursive is a checkbox in the Children title, beside the push it governs, rather than a question the form asks after the fact. Parents are the workspaces this one references and pulls from; children are the ones that reference it and get pushed to. Note this is the opposite of an assembly tree's sense, where the assembly is the parent of its parts — here the document a change starts in is the parent. The wording runs all the way down: the link directions, the pull scope, the graph walk, and the stored edge's two ends. The field that adds a link now sits under the list it adds to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A quick push no longer invents a name from the clock. It takes the one Onshape's own dialog offers — V<n> after the highest the document already has — worked out per document, so a recursive push numbers each from its own history rather than carrying the first one's number. The name is the server's to pick, since reading a document's versions is an Onshape call; the form leaves the field empty and says so. The page is now two accordion sections built like the home page's, each with its direction's arrow in the title and a Quick push or Quick pull button beside the control, where it is reachable with the section shut. Everything else moved into menus: the section's own carries the recursive push, naming a version, and updating every reference; each row's carries the same for that one link, plus opening the document and removing the link. The recursive checkbox is gone with them. Rows lead with a file icon. There is no recursive pull, and the row menu says why: a pull writes only to this workspace, so going further would mean versioning a parent's own parents, which is a push and theirs to make. Both scopes are enums now rather than bare strings, and the single-child push carries whether it keeps going, which is what a row's recursive push needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Quick push moves out of the button labels and into a checkbox on the page's own navbar row, where the search belongs on a library page. Checked, a push runs on the click; unchecked, it asks for a name first. A pull has nothing to name, so it always runs, and the tooltip says so. Every link now carries its own button beside the section's, so pushing to one child is a click rather than a menu. The menus keep what a button cannot say: the recursive push, and updating every reference. A run reports itself where it started. The button that began it holds a spinner and says what it is doing on hover, in place of the banner over the page; what the run did still arrives as a toast at the end, since a finished count is not something a spinner can hold. The naming form opens on the name the push would have used, read from Onshape through a new route rather than guessed at — offering a number Onshape has already handed out would be worse than the wait. Also: button icons sit after their labels, the accordion chevrons are at the end of their rows rather than against the titles, and a link the caller has no permission on says so in red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Two empty sections say neither what this page is for nor what to do about it, so before anything is linked the page is a welcome instead: what the version manager does, and the field that gets it started. The field takes a direction, since a first link is as likely to be a child as a parent, and the line under it explains which is which — the vocabulary the sections use as soon as one exists. The section buttons now say "Push to all" and "Pull from all", which is what they do to the lists under them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
…ng it The buttons now open a form, and the menus are where a run skips it. Push asks what the version is called and how far it travels; pull, having no version to name, asks how wide to cast — the parents somebody linked, or every out-of-date reference the workspace has, which is where "Update all references" now lives as well. Every menu carries Quick push or Quick pull, and a child's carries Quick recursive push, each running exactly what the form would have run from its defaults. Somebody who opens the form, changes nothing and pushes did not need it, so the tip says where the shortcut is. It follows the insert menu's tip: an info toast, raised only on an unedited run, quiet after three, and silent for good once they take the shortcut themselves. The navbar checkbox is gone with the mode it carried — recursive is a checkbox in the form again, beside the targets it changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Each section's line of explanation moves onto its title as an info bubble: it earns its room the first few times somebody reads it and never again, and the list starts where the header ends. A row's own button comes before its menu now, and is absent rather than disabled where Onshape will not have it — the row says why in red, and the menu shows the two runs greyed out, which is more than a dead button was saying. A link that cannot be opened says so in two lines, "Missing access" over what it is that cannot be done, in place of the name it is not allowed to show. The field that adds one asks for what it wants: an Onshape document link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Adding a group and linking a workspace both start with a pasted url, and both had their own half of the same parser. There is one now, in `lib/onshape-url.ts`: it reads whatever the url names — the document, the instance when there is one, the tab when there is that — and the two callers take the part they need from it. A leaf, so it is testable without reaching notifications or components, which is what the nine tests beside it do: a document-only link, a version, a microversion, a half-written instance, a configuration hanging off the end. The group flow's "Failed to parse url." becomes the wording the version manager already used, which says what to paste instead. Every Onshape call the version manager makes already went through a wrapper in `lib/onshape/endpoints`; each now carries the HTTP path it actually calls, since `apiPath` builds them and the url is not otherwise visible at the call site. `externalreferences` says why it is absent from the codegen config: the spec does not carry it. The routes say what they take, what they answer, and which Onshape permission they need — the part of a route's contract the code does not show. The reference doc gains the workflow, the feature, and the parser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The five it uses that Onshape's spec carries are in the codegen config now, and the committed reference dump is regenerated from them: creating a version, listing workspaces, reading a permission set, and updating an element's references, alongside the version listing that was already there. The dump confirms what was hand-authored from the 2024 Python rather than from Onshape: `referenceUpdates` really is a list of from/to document items carrying documentId, elementId and versionId; `permissionset` really answers bare strings; creating a version really takes name, description and the instance in the body and answers the version. Nothing had to change. `externalreferences` is confirmed absent from the spec rather than assumed to be — its comment now says so, having been tried. `defaultWorkspace` comes off the trim list while we are here: `getDocumentWorkspaces` pulls `BTWorkspaceInfo` in regardless, so keeping it costs two lines and confirms the `name` read off it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A link's row is a `CardTitle` and a `CardThumbnail` now, the same block the groups, favorites and search results are built from — which is what was making the first row a different height from its neighbours: a hand-rolled title with a bare icon where every other list has a fixed-size thumbnail. Every row measures the same, the one with no name included: a workspace nobody can read gets no thumbnail asked for and keeps the placeholder, which is the height anyway. `CardTitle` gains two slots to make that possible: a `subtitle` for a row whose second line is not a part number, and a `titleColor` for one whose name is itself the problem. Both are absent everywhere else, so nothing that uses it moves. The field that adds a link is a row of the same table rather than a card inset under it, which is what left it and the row above at their own heights. Workspace thumbnails come from Onshape directly, proxied because it serves them only to an OAuth caller. Nothing is stored, rendered or waited on — a workspace either has a picture or does not, and a document with none answers 404, which the shared component already shows as the question mark. Cached five minutes: neither immutable like a rendered configuration nor worth refetching per row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A row for a document the caller can read but not write to used to say so in red and hide its push button. The push route already refuses a run it cannot finish, before it cuts the first version, so that check was a second copy of one that has to exist anyway — and a wrong one whenever the permission changed after the list loaded. The row now pushes like any other and the refusal surfaces as the handled error it is. `canPush` goes with it, which also drops a permissionset call per linked workspace. Both section stacks move onto a shared AppSection: the version manager had its own copy of the library's accordion styles, plus padding around its panel that inset its rows from the grid every other list in the app sits on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Pushing was two clicks and a form even when the form had nothing to say: a version named V3 and the one child the row is. The button now runs it, and the form — a name, a description, the recursive box — is what clicking the row opens, with the menus carrying it too. Opening the linked document moves into the menu, which is where the rest of what to do with a link already was. Also: a color per part of the app rather than per library alone, since the dashboard and the version manager belong to neither — green and blue. The header row takes its own hover highlight, which the wrapper around the buttons was cutting short. The empty parent list's icon is colored like every other zero state's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Linking the first document was a segmented control, a line of text that swapped under it, and the list's own add row standing on its own. It is two questions — which way round the link goes, and which document — so it is now one card that asks them in that order: both directions as cards, each keeping its description so the one not picked still says what it would mean, then a labelled field and a button that names what it adds. The field is the same one the lists end with; only its dress differs, so both are built from one `useAddLinkForm`. A section's own empty state goes left-aligned: one line saying a list is empty should not take a list's worth of room. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The two options carried their own title and description; they now carry the push or pull arrow as well, on the same empty-state block the lists use, so the thing that runs the link is what marks it. The card stops at 480px, which only shows in a browser: in the panel it was already the whole width. The field goes back to its placeholder alone — a label saying what a placeholder already said. An empty list drops its period and takes the direction's arrow in the page's color, the broken-link icon having said only that something was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A link pasted into the wrong field was a delete and a re-add, which asks somebody to find the url again for a mistake they can see. The row's menu now turns it around: one row is one edge, so the move rewrites that row rather than removing it and hoping the insert lands. The exception is a pair that already links both ways, where turning this one around would collide with the row that is already there — the move then drops it, which leaves what was asked for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A pull moves this workspace's references onto a parent's latest version, so edits the parent has not versioned are edits it cannot bring in. Onshape counts them — `changesSinceVersionSave` on the insertables response — and each parent row now carries that count as a badge. Every `include` flag on that endpoint defaults to false, so asking for none of them is the cheap call: Onshape enumerates nothing and answers the counters. It is a query of its own rather than part of the list, being one call per parent, and a parent Onshape will not answer for is left out rather than failing the rest. The operation is in Onshape's spec but its response sits under `default` rather than `200`, so regenerating the reference dump adds nothing; the type here is hand-authored from the spec's own schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The app runs inside Onshape, so the browser showing it is already signed in there. The proxy route existed on the assumption that only an OAuth caller could read a thumbnail; if the session cookie reaches a cross-site request from our frame, the image element can fetch it and the bytes never touch the worker. Unverified against real Onshape — it is exactly the assumption that has to be tested. It fails safe: a refusal lands on the element and the row shows the placeholder a workspace with no thumbnail already shows. Such a url cannot be probed by the fetch the thumbnail component makes first, which is cross-origin and fails on Onshape's terms where the element would load. So an external url skips the probe, and `onError` carries the refusal instead — which also means a stored url whose bytes are gone now shows the placeholder rather than a broken image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The version manager moves onto what cert now does: Onshape endpoints as plain template paths with no `apiPath`, `getWorkspaces` from its own module, the notice component as `app-notice`, `useUiState` and the per-tab `useOnshapeLaunch`, `makeUrl` and the thumbnail url on the caller's own Onshape origin, a modal that carries its own id, and the section accordion on cert's sticky-header CSS rather than inline styles. Its migration is renumbered past cert's three, and its D1 test renamed to `.worker.test.ts`, which is what now decides that a test gets bindings. `createAppTheme` takes the color rather than the library, since the version manager's is blue and the dashboard's green whatever library is in view.
A push or pull was watched by asking every two seconds until it stopped. The server already has a socket to every client, so it says so itself: the route pushes the run as it starts, the workflow pushes how it ended, and the client asks once when the page opens and after a reconnect. A run belongs to an Onshape workspace rather than a library, so a client's socket is tagged with the workspace it was launched in as well — the message names somebody else's document, and only the people already in it hear it. `workspaceKey` moves to the contract, being what the graph walk, the stored job and now the socket tag all spell a workspace as. The finished-run toast takes an id, so a second run replaces the first's result rather than stacking under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The id changes with the name, since it is what the library is keyed by everywhere and a stored one is being reset anyway. Its rows move with it: a library's groups, insertables, favorites, jobs and every analytics table name it by id, and SQLite does not cascade an update, so the migration writes the new row, moves the children, and drops the old one. The program select still offers this library under FTC, and analytics still reads its season and vendors as FTC's: the rename is the library's name, not what is in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A tab strip said several libraries were open at once when one is, and it took the room the app's own pages need — the version manager was already scrolling off the end of it in Onshape's panel. The libraries move into a menu that names the one in view, and the tabs are left to the pages that are not a library's. The brand keeps its mark and drops its name in that navbar: the library beside it says where you are, and a few hundred pixels will not carry both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Onshape's api is at v17 now, and with it webhooks for a workspace being edited and for its metadata changing — which is what a linked workspace's row is made of. Opening the panel asked Onshape for every link's document name, workspace name and pending-change count every time, three calls a link and a fourth per parent. The names and the count are cached in KV, and a transient webhook on each linked workspace drops both entries when it is edited, renamed or versioned. Onshape drops a transient webhook that goes quiet, so the entries expire too — a week for a name, an hour for a count — and the next miss watches again. Permissions stay uncached: what a document is called is the same for everyone, what you may do with it is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
One document in the shape the others take: what a parent and a child are, the two flows and the order a recursive push runs in, every store and its lifetime, the invariants a change should be read against, and the decisions that look odd without their reason — a table of its own with no foreign keys, a workflow rather than the request, and the buttons running while the form sits behind the row. It ends with what is still unproven: nothing here has run against real Onshape, and `externalreferences` is absent from Onshape's spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Their page picker, illustrated zero state, versioning pull and per-row change count come over as they are; what was written against the pre-cert tree is ported onto cert's: the notice component, `useUiState`, the modal that carries its own id, `styles.module.css` in place of the style constants, and the Onshape endpoints as plain template paths. Where the two disagreed about the same thing, theirs wins: the thumbnail goes back through the proxy (`api-base.ts` with it), the change count moves into the links response rather than a route of its own, and the brand folds its wordmark by width rather than by a prop. Resuming in the version manager is cert's `tabId`, so their column and its migration are dropped; the entry redirect now checks the launch has a workspace, without which `/` and the version manager would have sent each other back and forth. The push-based job status, the KV caches and their webhook, the API version, and the `ConfigLib` rename all carry over onto their code. The page picker is the dropdown alone, as asked: the tab row it fell back to above 790px is gone.
…background - The page picker's menu carries the same blue dot on the version manager's icon as the picker button does, beside its New badge. - The Launch tunnel task reveals its terminal only on failure, so starting the servers leaves the dev server's output in front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
InfoTooltip takes the colour of what it sits in where that has one; a build-check callout passes its severity's, so an info check's icon is blue rather than the library's green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Configured thumbnails render only from a group's thumbnail workspace, but a load skips a group whose version is unchanged, so a group last loaded before thumbnail workspaces existed never got one. requestRender then answered "rendering" without starting anything, and the insert menu sat on the default thumbnail waiting for a push that never came. A load now skips a group only when it also has a thumbnail workspace (isLoaded), so any plain Reload branches one. A render asked for in the meantime is refused with a 503 rather than left waiting; the render can't branch one itself, since that takes edit access the caller may lack. The by-id fetch also passes skipDefaultImage, so an unrendered size fails and is retried rather than storing Onshape's stand-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Merging the earlier navbar redesign branch took its whole navbar, which predates cert's later edits to it. This puts those back: no library-load spinner in the navbar (rows show their own), the comment guidance pass, no aria-label on the clear button, the theme's search icon size, and an empty search query when none is stored. useIsLibraryLoading had no other caller, so it goes too. Audited the rest of cert against this branch line by line; every other difference is a deliberate change made here since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
A configured render that loads but shows the default means Onshape answered the configuration's thumbnail id with the element's own image, and the workflow stored it immutably. The workflow now compares what Onshape sends with the element's stored default and retries while they match, so a configuration only ever stores its own render; if Onshape never sends one within the minute, nothing is stored and the preview fails. requestRender logs the thumbnail id Onshape gives each configuration, and the workflow logs each default answer, so the logs show whether insertables ignores the configuration (one id for every configuration) or the by-id endpoint serves the default for a configured id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
URLSearchParams spells a space "+", which Onshape reads literally: a configuration like Length=0.1524 m reached insertables as 0.1524+m and was ignored without an error, so every quantity configuration got the default's thumbnail id and rendered the default. The Onshape client now builds its query with toQueryString, which spells spaces %20; a literal "+" is already %2B, so nothing else changes. Drops the default-bytes guard and the render log added to find this: the logs showed one thumbnail id for two configurations, and the guard never fired, since Onshape's default-configuration render is not byte-identical to the stored default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Focusing the box swapped its rounded display for the full expression after select() had run, and changing an input's value drops its selection, so a click on a value shown rounded left the caret at the end. The selection now happens in a layout effect, once the expression is in the box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
On each render request, asks insertables for the thumbnail id with no configuration and with the configuration spelled once-encoded, double-encoded (as the old Python app sent it) and with "+" for spaces, against both the thumbnail workspace and the version, and logs them all. Revert once the logs show which one Onshape reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Probing insertables showed the thumbnail workspace answering three different configurations (Length=0.1524 m, Length=0.161925 m, and an enum plus a length) with the same predictableThumbnailId, distinct from the unconfigured one, so every configuration rendered as the same image. On the version the id moves with the configuration, which is where the original app asked. Double encoding is a 400 and "+" reads like %20, so the encoding was never it. This is a design change: configuration ids are now resolved on the version while element and group thumbnails stay on the thumbnail workspace, and the thumbnails doc's invariant and decisions say so. Renders no longer need a workspace, so a group without one is no longer refused and loads stop reloading unchanged groups to branch one; the probe goes too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Onshape's own dialog polls /thumbnails/d/{did}/w/{wid}/e/{eid}/c/{key}/s/{size}
until it stops answering 404, where the key is base32 of a FeatureScript map
of the configuration (a quantity's base value as its double's bits, an enum
option with its type). Insertables returns that key as the response's
top-level configurationKey, so a render asks insertables in the thumbnail
workspace for it and polls that url. The predictableThumbnailId flow goes:
in a workspace it named one id for every configuration, and on the version
the workspace's reliable rendering is lost.
This puts back what rendering in the workspace needs: a load branches a
thumbnail workspace for a group without one, and a render for such a group
is refused rather than left waiting. Render instances take a new id prefix,
since an older instance restarted would carry the old parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Logs the top-level fields of the insertables response a render reads its configuration key from, with the key base32-decoded, and each failed try at the configured thumbnail. Revert once the logs show why renders time out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Onshape names a configured render by the double it computes from what was typed: 2.25 in is 2.25 * 0.0254 = 0.057150000000000006, while the key's 0.05715 m is 0.05715, one bit apart. Sending the key's base-unit spelling made insertables name a render Onshape's dialog never asks for, so ours was never found; 5 in worked only because 0.127 is exact either way. The render request now carries the selection. The server makes it whole, derives the key itself (so a caller can no longer store one configuration's picture under another's key), refuses the element's default, and sends Onshape renderOverrides: the overrides as entered, less derivation variables. toKey is built on the same overrides. This tightens an invariant: a key is never sent to Onshape as a configuration, a render's included, in configurations.md and AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Onshape's insert dialog asks /c/{key}/s/{size} with t (the microversion) and
rejectEmpty=true; the render workflow now sends the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
…nder
Run with an Onshape API key. Each probe picks a length nobody has opened,
asks insertables for its configuration key, makes one candidate call (none,
parts, parts with thumbnails, configurationencodings then the /ac/ thumbnail,
or partstudio shadedviews), then polls /c/{key} for two minutes and reports
how long it took to load. Delete once the render path is settled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Back to how prod renders, in a workflow rather than its Durable Object queue: a configuration's thumbnail id is resolved on the insertable's version (the selection as entered, escaped once), and the workflow fetches it by id, one size at a time, since asking Onshape for a second render abandons the first. Element and group thumbnails are read from the version and, when it won't give one up, from the document's own workspace, during a load and on Reload thumbnail alike. The per-version thumbnail workspace goes: no load branches or deletes one, groups.thumbnail_workspace_id is dropped (migration 0009), a group is no longer reloaded for lacking one, and createWorkspace/deleteWorkspace go with it. Load thumbnail steps return to three retries 10-40 s apart, since the 16-minute schedule was for fresh branches. The /c/ configuration-key render, its temporary logging and the probe script are removed. This is a design change: the thumbnails doc's invariant that every thumbnail is read from the thumbnail workspace is replaced by version-first reads, and a render workflow asking for one size at a time is a new invariant; both are under Decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The render workflow asks Onshape for each size every 2 seconds rather than every 5, still for about a minute (30 retries), so a render that lands is picked up sooner. A 429 still waits its Retry-After. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
One section, stage by stage: the Onshape client, a load's default thumbnails, Reload thumbnail and its toasts, serving, a configuration render on the server and the client, stored images on the client, push reconnects, and the insert menu's wait tip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The section now walks through each situation (a load, browsing, picking a configuration, a saved favorite, an editor's reload, a lost connection) and says how long the app keeps trying, what the person sees meanwhile, and what happens when it gives up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The entry route redirected a resumed utility tab by href, which carries its own search and dropped the one passed beside it, so a panel opened with the version manager last lost Onshape's launch: the page found no workspace and sent it to the library, and with the launch adopted only once per load, the page picker never offered the version manager again in that panel. It now redirects by route, as a library tab does, so the launch rides along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
- Version name is required and opens with the suggestion filled in; the form sends no name while it is the untouched suggestion, so each document a recursive push versions is numbered from its own history. - Focusing a version form field or the link url selects its text. - Linking a document already linked either way is refused with a 409; the success toast names the linked document and workspace. - Linking also clears the version manager dot. - Toolbar refresh button: forgets the linked workspaces' cached names on the version manager, reloads the library elsewhere. - Insert location is hidden while the version manager is showing. - Shorter parent/child tooltips; toasts end with a period throughout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
- Recursive pull (PullScopeKind.ANCESTORS, or ONE with recursive): the closure above this workspace in topological order (pullOrder), each parent moved onto its own parents' new versions and then versioned, this workspace last. Offered as "Quick recursive pull" and as the pull form's Recursive option. Design change: the doc said there was no recursive pull; it now describes one, with the reason under Decisions. - collectEdges walks links either way; graph.ts shares one post-order walk between pushOrder and pullOrder. - A run forgets the cached change count of each workspace it versions or moves, so a parent's changes badge updates once the run ends. - The page's loading and error states are full-page notices (PageLoading). - The version name is focused and selected once the suggestion arrives. - The refresh button is shared with the dashboard (RefreshButton), spins in the page's color, toasts when done, and shows on the version manager alone. - Drop the summary pills from the run details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
- Bump every @Mantine package to 9.7.0 (charts stays pinned exactly). - AppHoverCard is one HoverCard for mouse and touch: 9.7's `events.touch`, on by default in the theme as a Tooltip's is, replaces the (hover: hover) query and the Popover it fell back to. - Cards holding controls (insert location, build status) are `interactive`, which 9.7 needs to keep one open while the pointer moves onto it. - `onDismiss` swallows the click that ends a dismissing tap, so a tap outside a card closes it without opening the row underneath; mouse clicks and Escape pass. - Chrome follows a tap's click with a mouseleave, which floating-ui closes the card on; `holdOpenAfterTap` drops that one event so a tapped card stays until dismissed. useAppShellResize is left out: it drives drag-to-resize sections, and the shell's only section is a header sized by its content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
withoutDeprecated drops, from a document's contents as the load reads them, every tab whose name contains "deprecated" (any case) and every tab inside a folder so named, at any depth. Tab selection, removal, ordering and the document thumbnail all read the filtered contents, so a stored insertable whose tab is renamed or moved into such a folder is removed like a deleted one. OnshapeElementGroup gains Onshape's `groupName`. Configuration options and parameters are not filtered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
createShellGroup writes the group with a NOT_LOADED build issue, so a document whose first load never starts (requestLoads failing after the shell is written) still shows something wrong and asks for a Reload. The first successful save recomputes the group's issues, clearing it; a failed load replaces it with LOAD_FAILED. The build card treats it as it does LOAD_FAILED: a group never loaded has nothing to unhide. Also drops the version manager's reference to the removed job-tracker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
The name read too close to an existing push service. Migration v3 renames the class in place (renamed_classes), so the namespace and its sockets carry over; the binding is now CLIENT_RELAY and the module features/push/client-relay.ts. The push feature and its messages keep their names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
…sion held Design change: replaces one LoadDocumentWorkflow per group, and the load_jobs table, with one ReloadLibraryWorkflow per request, scoped to the group ids it names. loading.md's invariants and decisions are rewritten to match. - One reload holds a library at a time (`library-reload:` in KV); one asked for meanwhile sleeps 30 s between claims until the library is free, so nothing is queued to be lost and no group is loaded twice at once. A holder found dead is taken over and its unfinished groups flagged LOAD_FAILED. KV has no compare-and-set: two reloads claiming a free library in the same instant both run, which is accepted. - Every group of a reload runs at once under its own step names (`prefixSteps`) and shares one Onshape budget, LOAD_CONCURRENCY now 30 across the reload; the search index is rebuilt and the version bumped once, at the end. The workflow's step limit is raised to 25,000. - Job status comes from the claim's remaining groups and the queued reloads (`library-reload-queued:`), checked against Workflows on read. - Every webhook version is held: there is no longer an approval setting. A held reload waits up to a week for Approve, or an admin's Reload, then loads anyway. - Migration 0010 drops load_jobs and libraries.approve_versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
Production is at 0004; cert and local databases are disposable. The six migrations after it (0005_library_loading through 0010_reloads_in_kv) created load_jobs, approve_versions and thumbnail_workspace_id only for later ones to drop them, so they fold into one, 0005_teams_webhooks_and_version_manager: drizzle's diff from 0004, with the events rebuild copying only the columns 0004 has, then the ConfigLib rename and the analytics backfill carried over by hand. The schema it leaves is identical to the old chain's, and seeded 0004 data upgrades correctly. reset-db.sql now drops every table, including load_jobs, so cert can be reset before this deploys: its d1_migrations already records the old 0005. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
…ad-state gaps Access: - Reload (outdated documents) and approving held versions are an editor's, since editors see held and failed documents; forcing every document stays the owner's. auth.md's table moves them. Thumbnails: - requestRender settles the selection as the insert menu does (normalizeSelection) before deriving its key, so the key always matches the one the menu waits on, whatever the caller sends. - The render workflow falls back to an admin's session of the insertable's library once the requester's stops working: a restart reruns with the first requester's session, so a render once asked for by someone since signed out could never finish. - Reload thumbnail overwrites each size only once Onshape gives up a new image, rather than deleting first, so a failed reload keeps the picture the row names. A group's tab is picked as a load picks it (documentThumbnailElement, deprecated tabs left out), and a group that has never loaded is refused. The default-workspace lookup is shared (toDefaultWorkspacePath). Reload state: - A failed read of a reload from Workflows is taken as live, not dead: every mark names a reload that exists, so a blip no longer abandons a running reload or drops a held one from the approve list. requestReload removes its mark if the instance can't be created, which retires the one-minute grace period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Links Onshape workspaces to each other and moves external references
between them: a pull updates this workspace's references onto the newest
versions of what it consumes, and a push cuts a version here and moves
the references of the workspaces that consume it onto it.
Rebuilt from the implementation this repo carried before the rewrite
(5fb1ff5, Jan 2024), with three changes:
than in a Firestore collection holding both ends separately, where a
link could half-exist when only one of the two writes landed.
The old one walked the whole chain, cutting a version in every
document along it; that is now the opt-in recursive push, which
needs those versions for the next hop to reference.
rate limit backs off and resumes rather than leaving a version cut
and half the references moved.
The page is a tab beside the libraries, shown only when the panel was
opened in a workspace, since that is what it acts on.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012VDUXqzz4JkJeiauwQweyk