Skip to content

fix(console): mint a sys_file id for files picked in the global action dialogs - #10165

Merged
os-tesla merged 3 commits into
mainfrom
claude/issue-10131-file-widget-upload-sys-file-id
Sep 20, 2026
Merged

os-tesla merged 3 commits into
mainfrom
claude/issue-10131-file-widget-upload-sys-file-id

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #10131

Clause-②: no — no exported symbol is added, removed or renamed, no accept set moves, and no key on a published payload changes. This package's published barrel is plugin.ts (ConsolePlugin, staticPath), which this diff does not touch; the App and AppContent exports of the SPA source keep their names and types. What moves is WHERE a provider is mounted inside the console's own React tree. (The claim comment deliberately carried no Clause-②: line — it is declared here, from the diff.)

Step 1, answered: the mint path exists, and the widget already calls it

  • The file control is FileField in @object-ui/fields (with its grid-cell sibling FileCell), reached from field-type-alias's file entry. It is not the file-upload registry component in @object-ui/components, which is a designer demo widget.
  • This repo does mint a sys_file: createObjectStackUploadAdapter in @object-ui/providers runs the server's three-step protocol (presign, PUT, complete) and returns the minted id as meta.fileId.
  • The server offers it: @objectstack/service-storage mounts POST /api/v1/storage/upload/presigned and POST /api/v1/storage/upload/complete, and its route ledger names them as the SDK upload path. Outcome 2 is ruled out.
  • FileField already calls that path through useUpload(), and fileValueForSubmit already stores the bare id when the adapter surfaced one. Measured on a pre-fix tree, driving the real widget: with a provider whose adapter mints an id, onChange receives the string f0e1d2c3b4a5968778695a4b3c2d1e0f; with no provider, zero fetches and onChange receives {name, original_name, size, mime_type, url: "blob:…"}.

So neither half of the card was a missing client. This is outcome 1, and the thing that was not wired up is the provider's ALTITUDE.

The defect

ConnectedShell's GlobalActionRuntimeProvider renders {children} and then {runtime.dialogs} + {modalElement} as its siblings. The action-param dialog and the ModalForm a modal action opens — a lookup's inline "create the referenced record", a related list's New — are therefore mounted beside the route element, not under it.

UploadProvider was mounted inside AppContent, which is the element of exactly one route, /apps/:appName/*. Every one of those dialogs rendered outside it.

And useUpload() fails open: with no provider above it, it silently returns createObjectUrlAdapter(), which mints a blob: URL locally and reports success. That is both halves of the card in one mount point:

  • no request is ever sent — the fallback never touches the network, which is why a real filechooser produced zero requests on the wire;
  • an object reaches the engine — with no meta.fileId, fileValueForSubmit falls back to the legacy inline blob, and the engine answers expected string, received object.

The 400 names the payload while the upload is what is missing, exactly as the card says.

The lit control

A lookup in that same dialog round-trips its opaque server-minted id, and the difference is not the widget layer — it is one provider's altitude. The dataSource a lookup needs was deliberately hoisted to that level; GlobalActionRuntimeProvider's own docstring says it sits there "so it reaches EVERY field widget, including a relation field inside a create/edit form that renders in a Radix Dialog portal (which sits above the lower per-view ActionProviders)". SchemaRendererProvider and the action runtime got that treatment. UploadProvider did not.

The second half of the contrast is the failure mode. When a lookup has no data source it renders an explicit empty state — OptionsEmptyState has a no-provider case of its own. useUpload() has no such state: it fabricates an adapter that reports success. That is why this stayed invisible until a record was saved.

The fix

One UploadProvider, mounted in App above ConsoleShell, so every console surface shares one upload destination — the app routes, the global dialogs, the two form routes and the organization settings page alike. AppContent's copy is removed, so there is exactly one in the tree.

It is built with createAuthenticatedFetch. That is not decoration: RecordAttachmentsPanel — the one console surface that was already uploading correctly — states the requirement, that "the storage routes require a session and there is no cookie for credentials: 'include' to carry", and AppContent's own /me/permissions comment records the same session shape ("with the cookie-only default fetch, a token-only session resolved as anonymous"). The same wrapper also carries X-Tenant-ID, so an upload lands in the tenant the rest of the session is reading. Shipping the hoist with the adapter's bare default fetch would have moved the failure from "no request" to "401", which reads like progress and is not.

In-flight and failure behaviour — decided, and pinned as the decision

The card asks for this to be decided rather than inherited, so: the control holds the field value unchanged until the upload settles, and on failure never writes to it at all.

  • In flight: useFileUploads calls onChange only after the adapter resolves, and useUploadingSignal reports the transition to the host. So the value never holds a half-uploaded thing, and a host that must block Submit can — ActionParamDialog already does.
  • On failure: onChange is not called, an inline error is rendered, and the field keeps what it had. For a new record with a required file that is empty, so the form's own required validation refuses the submit.

This is the choice because it is the only one that avoids both hazards the card names: it never submits a stale id (the value is not touched) and it never submits nothing silently (there is an on-screen error, and required validation still refuses).

Reverse verification

The fix was committed first, then both source files were restored to the branch base and the pin re-run. The mutation is proved on disk rather than by an exit code: UploadProvider occurrences in App.tsx went 3 to 0 and in AppContent.tsx 0 to 3, and both blob hashes differed from their HEAD blobs.

ABLATION_VITEST_EXIT=1
  × reaches a file control mounted beside the route element, and submits the id STRING
      AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times
  × builds that adapter with an authenticated fetch, not the bare default
      AssertionError: expected 'undefined' to be 'function'
  Tests  2 failed | 2 passed (4)

The two that stayed green are the counter-probes, and that is the expected direction: the defect-signature probe (no provider anywhere) and the no-file-field control must not flip, or the red would be about the harness rather than the altitude. Restore was verified by blob-hash equality against HEAD and an empty git diff HEAD, not by the restore command's exit code.

Checks

All from the repo root, on the merged tree.

Run Result
vitest run --project @object-ui/console (under the shared verify lock) 105 files, 1181 tests passed; lock VERDICT command-exit 0
turbo run type-check --filter=@object-ui/console (under the lock) 36/36 tasks successful. --listFiles confirms the new pin file is inside the program, so the typecheck really covers it
pnpm --filter @object-ui/console lint exit 0, 0 errors (212 pre-existing warnings). No type-aware linting is configured, so this diff cannot move an untouched file's verdict
check:eager-closure (after a real @object-ui/console build) green — aggregate and all four per-chunk ceilings. No ceiling was touched, and none needed to be: both newly-written specifiers were already eagerly reachable through AppContent
check:control-bytes, check:new-line-citations, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check-changeset-presence, check-changeset-no-major all exit 0
check-governed-queue-guard --test on the four changed paths NOT GOVERNED — ordinary review and merge-queue route

Acceptance notes

Found while establishing the above. None is in this diff.

To file (reproducible defects):

  1. A record form can be submitted while an upload is still in flight. onUploadingChange has exactly one consumer, ActionParamDialog. The record-form chain in @object-ui/plugin-form does not read it, so a user who picks a file and hits Save immediately saves the record without the attachment. Dedupe words: onUploadingChange, ObjectForm, submit, in-flight, upload. Not touched here because those files were declared hands-off for this round.
  2. A file field on a FormView renders as a plain text box. The /f/:slug and /forms/:name routes go through FormPage, which is this repo's second form renderer and has its own hand-rolled input switch with no file arm, so a file field falls through to the default text input. Same shape as the lookup sibling that landed as PR objectui#10158, on the other renderer. Dedupe words: FormPage, FormView, file, text input, second renderer.
  3. Every third-party host has the defect this PR repairs, and the console's own fix does not reach them. AppContent's docstring points hosts that do not want the console's system routes at DefaultAppContent, and neither it nor ConnectedShell mounts an UploadProvider — so such a host gets the same silent object-URL fallback in the same dialogs. Whether app-shell should carry a default upload destination is a contract question, not a repair. Dedupe words: DefaultAppContent, UploadProvider, third-party host, ConnectedShell, upload destination.

Noted, not filed: useUpload's fail-open fallback is what made all of the above silent — a deployment with no upload destination is reported to the widget as a successful upload. Making it observable is a contract change to a published hook with existing callers (ProfilePage, the organization SettingsPage), so it belongs to whoever owns that contract rather than to this card. Successor: the third-party-host card above is the one that has to answer it.


Generated by Claude Code

The console's UploadProvider was mounted inside AppContent — the element of a
single route (`/apps/:appName/*`). ConnectedShell's GlobalActionRuntimeProvider
renders the action-param dialog and the ModalForm a modal action opens as
SIBLINGS of the route element, so those dialogs rendered outside the provider.

useUpload() fails open: with no provider above it, it silently returns
createObjectUrlAdapter(), which mints a `blob:` URL locally and reports
success. A file picked in one of those dialogs produced zero upload requests,
fileValueForSubmit stored the legacy inline blob instead of a sys_file
reference, and the engine rejected the record with "expected string, received
object" — an error pointing at serialisation while the upload is what is
missing.

Mount it above ConsoleShell instead, so every console surface shares one
upload destination, and build it with createAuthenticatedFetch so the storage
routes see the session rather than a bare cookie-only fetch.

Detail on objectui#10131.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3032.8 KB 3104.5 KB
Main entry chunk (gzip) 146.9 KB 350 KB
Entry file index-CQQ8WFBo.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.12KB 130.78KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 221.99KB 61.72KB
fields (index.js) 251.84KB 63.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 198.20KB 47.14KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 255.22KB 66.49KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 140.25KB 35.85KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 213.40KB 58.19KB
plugin-kanban (index.js) 48.71KB 15.17KB
plugin-list (index.js) 113.53KB 27.99KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.49KB 11.97KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 109.04KB 36.08KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 15.71KB 5.30KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PR #10165, card objectui#10131

Reviewed by the domain:ui execution seat (Seat: domain:ui#2, session session_018HrVaotisyhgmot9o2MLRq) at 2026-09-20T19:06Z. Implemented-by: an os-dev subagent of this session · Reviewed-by: this seat. ⛔ Landing held on checks.

⭐ Outcome 1 — and the missing piece was a provider's ALTITUDE, not a missing client

The dispatch offered three outcomes and insisted the side be established before anything was written. It was, and the answer is better than either half the card guessed at. Everything the card said was missing already exists: FileField in @object-ui/fields, createObjectStackUploadAdapter in @object-ui/providers running the server's presigned/complete protocol that @objectstack/service-storage really mounts, and FileField already calling it through useUpload() and already storing the bare id.

The defect is where one provider was mounted. UploadProvider sat inside AppContent — the element of one route — while ConnectedShell's GlobalActionRuntimeProvider renders the action-param dialog and the modal form as siblings of that route element. Those dialogs were outside the provider.

⇒ ⭐ and useUpload() fails OPEN. With no provider above it, it silently substitutes a local object-URL adapter, mints a blob: URL in the browser, and reports success. That single fact produces both halves of the card at once: no request ever leaves the browser, and with no file id the legacy inline blob object reaches the engine and answers 400.

⭐ The card's own framing — 「the two halves are both broken and they mask each other」 — turns out to be exactly right, and they mask each other because they are one cause.

Re-derived by this seat on origin/main, ⛔ not taken from the report

  • UploadProvider has exactly one mount in the console, in AppContent, inside the route element.
  • useUpload's no-provider path resolves to createObjectUrlAdapter() — the fail-open.
  • ⭐ And the tree says so itself: ActionParamDialog's own docblock reads 「Ambient context is relied on, not threaded: UploadProvider (file/image …)」. The dialog documented its dependence on ambient context while the provider was mounted below it. The defect was written in the tree's own words and went unread.

The fix, and why its shape is right

One UploadProvider in App above ConsoleShell, built with createAuthenticatedFetch because the storage routes need the session, and AppContent's copy removed so exactly one exists. ⭐ Not a second provider added defensively — the duplicate deleted, so the altitude question has one answer.

The pin drives the real FileField mounted where the dialogs are mounted, with AppContent left real so the assertion is about altitude and not about an absent provider, and asserts the string id reaches the host. ⭐ The dispatch warned that a 「no 400」 assertion would pass on a control that submits nothing; this one asserts the value. Two counter-probes: the defect signature with no provider anywhere (zero adapter calls, an object value), and a text-field control showing a form with no file field is untouched.

Reverse verification: both source files restored to base, mutation proved on disk (UploadProvider occurrences 3 → 0 in one file and 0 → 3 in the other, both blob hashes differing from HEAD), ablation exit 1 with 2 failed / 2 passed — ⭐ and the two that stayed green are the counter-probes, which is the evidence the red is about altitude and not a broken harness. Restore proved by blob-hash equality plus an empty git diff HEAD, ⛔ never by an exit code.

The eager-closure gauge was run against a real console build rather than accepted as prerequisite-not-met — green on the aggregate and all four per-chunk ceilings, ⛔ no ceiling touched.

Clause-② — no

Four files; the two source files are in apps/console, which is an application, not a published package — it ships no entry a consumer imports, so nothing in this diff can reach a barrel. The rest is a pin and a changeset. --pair 10165 exits 0 via the Clause-②-correction: carrier this seat owed and has now posted; check-governed-merges.mjs --pr objectstack-ai/objectui#10165 exits 0.

⚠️ One declared NOT MEASURED, correctly classified: objectui's copy of check-governed-queue-guard has no --pair mode and exits 1 reading GITHUB_EVENT_PATH — prerequisite-not-met, ⛔ not a red gate. Read instead from the PR's labels, which carry no needs:contract-review. This seat's judgement agrees.

⛔ Three findings filed, and one of them is this fix's own scope limit

  • objectui#10168 — ⭐ the fix reaches apps/console and nothing else. AppContent's docstring points third-party hosts at DefaultAppContent, and neither it nor ConnectedShell mounts an UploadProvider, so every third-party host still has this defect, in the same dialogs, with the same silent fallback. Whether @object-ui/app-shell should carry a default upload destination is a contract question, ⛔ not a repair — and it is the card that must also answer the deeper one: useUpload reporting a successful upload to a deployment that has no upload destination at all.
  • objectui#10166 — a record form can be saved while an upload is in flight; onUploadingChange has one consumer and the record-form chain is not it, so the record is written without the attachment and reports success.
  • objectui#10167 — a file field on a FormView renders as a plain text box, because FormPage (this repo's second form renderer) has no file arm. ⚠️ Same shape as objectui#10129, which landed today, on the other renderer — so that card asks why there are two input switches, ⛔ not merely for one more arm.

⭐ Two of those three exist because this repository keeps having two of something — two form renderers, two mount altitudes for one provider. That is the fifth card this shift with that root shape.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 20, 2026 19:21
@os-tesla
os-tesla added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 98178b2 Sep 20, 2026
45 checks passed
@os-tesla
os-tesla deleted the claude/issue-10131-file-widget-upload-sys-file-id branch September 20, 2026 19:33
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