Uh oh!
There was an error while loading. Please reload this page.
feat(app-shell): Setup page for packaged automation — on/off + clone (ADR-0126 §7.4) - #6382
Merged
os-support-ai merged 4 commits intoAug 25, 2026
Merged
Conversation
Adds the operational Setup surface for the flows an installed package ships (ADR-0126 section 7.4). Automation authoring stays in Studio; this page holds only what the activation ledger knows. Reached through the component-registry nav contribution, the way every other framework-contributed Setup surface is: `services/builtinComponents` registers `automation:packaged`, app navigation names that ref, and `ComponentNavView` resolves it at `/apps/<app>/component/automation/packaged`. No bespoke route is added — a second way in would be a URL the app metadata does not know about. Per packaged flow, exactly two actions: * on/off for this scope — reads the engine's activation state (`GET /automation/_status`, backed by the section 7.2 `sys_metadata_activation` ledger), flips it via `POST /automation/:name/toggle`; * clone — `POST /automation/:name/clone` with a mandatory new machine name and label (section 7.1). The carried-over definition is never an editable form field; the copy is edited in Studio like any other flow. The list is scoped to packaged flows by the server's own three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6), transcribed in `packagedFlows.ts` rather than shortened to the `_packageId` sentinel — that shortcut classifies a tenant overlay bound to a package as packaged (the cloud#970 misread), which here would put a tenant's own flow behind an install-wide switch. Server refusals reach the operator verbatim, with no client-side softening: the section 5 posture gate (403 PERMISSION_DENIED, naming the posture and the sanctioned clone path), the section 7.3 subflow guard (409 DELETE_RESTRICTED, naming the packaged callers that would break mid-run) and the section 7.1 clone name conflict (409). `actionErrorDetail` is the single reader; a fallback string is used only when the response carried no message at all. No drift or ancestry surface (section 9): no diff-vs-base, no "customized" badge, no base-moved notice, no link from a clone to its source. Cloned-without-disabled and disabled-without-clone are ordinary states shown plainly. Tests pin the absence, including a response that carries a `clonedFrom` key anyway — proving the rule is enforced at the renderer and not only on the wire. Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
…stays light
`apiBase()` lived in `views/metadata-admin/previews/useFlowNodePalette.ts`,
whose module scope reaches `flow-canvas-parts.tsx` and the whole flow-designer
canvas below it. That costs nothing for the two modules already importing it
(the palette fetch and the runs panel, both inside the designer), and costs a
chunk for one that is not: the new packaged-automation page is imported EAGERLY
by `services/builtinComponents`, so everything in its graph joins the console's
eager closure that `check:eager-closure` weighs.
Three lines of URL arithmetic now live in `utils/apiBase.ts`, a leaf.
`useFlowNodePalette.ts` imports and re-exports the name, so its existing
importers are untouched and there is still exactly ONE definition — the repo
already carries several hand-rolled copies of the same `VITE_SERVER_URL`
arithmetic, and a seventh was the wrong way to avoid the drag.
Imported and then exported rather than `export { apiBase } from …`: that module
calls `apiBase()` itself, and a bare re-export forwards the name without binding
it in local scope.
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdChContributor
✅ Console Performance Budget
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
Size Limits
|
…ten packs
`check-i18n-call-site-keys.mjs` failed the Type Check job: all 22 keys the new
Setup page names exist in no locale pack, so every string on it was reachable
only through its inline `defaultValue`. That renders English at the call site
and leaves the page untranslatable everywhere (objectui#3517) — the gate says
in as many words that an inline default is NOT a fix.
Adds the `packagedAutomation` group to `packages/i18n/src/locales/en.ts`, the
source of truth, and real translations to the other nine packs. Terminology
follows each pack's existing vocabulary rather than being invented here — the
words for "flow" and "package" are taken from `flowRunner.completed` and
`common.package` in that same pack (zh 流程/软件包, ja フロー/パッケージ,
ko 플로우/패키지, de Flow/Paket, fr Flux/Package, es Flujo/Paquete,
pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة), as is each one's quotation
convention.
The group deliberately carries no drift or ancestry wording (ADR-0126 section
9): a translatable string for a lineage the platform does not track is the
cheapest way for that surface to reappear. It also carries no refusal prose —
the posture gate, the subflow guard and the clone name conflict are
server-authored and rendered verbatim; the four `*Failed*` keys are last-resort
fallbacks for a response that carried no message at all.
Two call-site changes the keys required:
* `toggleFailed` and `cloneFailed` each split in two. The response arm has an
HTTP status to name and the transport-exception arm does not, and one key
cannot carry a hole only half its call sites can fill — the gate's
interpolation-parity leg checks exactly that.
* `cloneCreated` and `toggleLabel` now spell their holes `{{name}}` /
`{{label}}` instead of interpolating in a JS template literal, so the inline
default is byte-identical to the value the pack serves and a provider-less
render cannot drift from a translated one.
`de-quote-pairing-3876.test.ts` moves 53 -> 54 on both of its counts. The German
`cloneCreated` adds one MATCHED „…“ span around an interpolated hole, the same
shape `flowRunner.completed` contributed; `rdq` stays 0, which is the half of
that ratchet that carries the meaning.
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh…kaged-automation-setup-page
Contributor
✅ Console Performance Budget
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
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 25, 2026 19:32
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-6301-packaged-automation-setup-page
branch
August 25, 2026 19:44
This was referenced Aug 25, 2026
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 freeto 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.
Closes#6301
Part of objectstack-ai/objectstack#12150
Adds the operational Setup surface for the flows an installed package ships (ADR-0126 §7.4 + §9, objectstack
28b47a93). Automation authoring stays in Studio; this page holds only what the activation ledger knows.How it is reached
Through the component-registry nav contribution — the mechanism every other framework-contributed Setup surface uses (
metadata:directory,metadata:resource,developer:packages).services/builtinComponentsregistersautomation:packaged; app navigation names that ref andComponentNavViewresolves it at/apps/<app>/component/automation/packaged.⛔ No bespoke route is added. A second way in is a URL the app metadata does not know about, and the nav contribution is the mechanism the card asks for.
What the page does, and what it deliberately does not
Per packaged flow, exactly two actions:
GET /automation/_status, backed by the §7.2sys_metadata_activationledger); flips it viaPOST /automation/:name/togglePOST /automation/:name/clonewith a mandatory new machine name and label (§7.1)The carried-over definition is never offered as editable form fields (the #11753 discipline) — the request body carries exactly
{ name, label }, pinned by a test. The copy is edited in Studio like any other flow.The page shows no
bound/statuscolumn even though_statuscarries both: §7.4 scopes it to what the activation ledger knows, and the Studio rail is where a flow's binding is diagnosed.⛔ No drift or ancestry surface (§9)
No diff-vs-base, no "customized" badge, no base-moved notice, no link from a clone back to its source. Cloned-without-disabled and disabled-without-clone are ordinary states, shown plainly.
The sharpest test feeds a clone response that carries
clonedFromandbaseVersionanyway and asserts neither reaches the DOM — pinning the rule at the renderer, not only on the wire. A response field is the cheapest place for ancestry to reappear. The rule is carried into the locale packs too: thepackagedAutomationkey group has no lineage vocabulary in any of the ten, because a translatable string for it is the cheapest way for that surface to come back.One distinction the tests draw explicitly: the scan for withdrawn shapes deliberately excludes
/copied from/. The server's own post-clone notice ends "…will run alongside the flow it was copied from", and that notice is relayed verbatim. §9 withdraws a lineage surface this page would have to invent and maintain; it does not censor the server's sentences. The first draft of the test scanned for the phrase and failed on the platform's own words.Server refusals reach the operator verbatim
No client-side softening, shortening or rewording.
actionErrorDetail(this repo's one ADR-0112 envelope reader) is the single reader; a fallback string is used only when the response carried no message at all. Three shapes, each pinned as the exact server string:PERMISSION_DENIED— the §5 posture gate. The message names the tenancy posture and the sanctioned clone path; a summarising client would drop the half that says what to do instead.DELETE_RESTRICTED— the §7.3 subflow guard, on disable only. The message names the packaged callers that would break mid-run — a list nothing on the client could reconstruct.Scoping to packaged flows
packagedFlows.tstranscribesisCodeArtifactBody(@objectstack/objectqlregistry.ts, ADR-0029 D9.6) clause for clause rather than shortening it to the_packageIdsentinel. That shortcut classifies a tenant overlay bound to a package as packaged — the cloud#970 misread — and here it would put a tenant's own flow behind an install-wide switch. The counterexample is pinned by test, not just the easy cases.The runtime list is the spine (it holds every flow the engine can actually toggle or clone); the metadata list answers packaged-ness and the label.
Three commits, and why the second and third are here
7306a81movesapiBase()out ofviews/metadata-admin/previews/useFlowNodePalette.tsinto a leafutils/apiBase.ts. That module's scope reachesflow-canvas-parts.tsxand the whole flow-designer canvas below it — free for the two importers already inside the designer, a chunk for this page, whichservices/builtinComponentsimports eagerly and is therefore in the console's eager closure (check:eager-closure).useFlowNodePalette.tsimports and re-exports the name, so its existing importers are untouched and there is still exactly one definition; the repo already carries several hand-rolled copies of the sameVITE_SERVER_URLarithmetic and a seventh was the wrong way to avoid the drag.2eb4fb4fixes the Type Check failure this PR's first push produced.check-i18n-call-site-keys.mjsfound that all 22 keys the page names existed in no locale pack, so every string was reachable only through its inlinedefaultValue— English at the call site, untranslatable everywhere (objectui#3517). ThepackagedAutomationgroup now lives inen.tsand in real translations across the other nine packs, with terminology taken from each pack's ownflowRunner.completedandcommon.packagerather than invented (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket, fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة), each one's quotation convention included.Two call-site changes the keys required:
toggleFailed/cloneFailedeach split in two, because the response arm has an HTTP status to name and the transport-exception arm does not, and one key cannot carry a hole only half its call sites can fill; andcloneCreated/toggleLabelnow spell their holes{{name}}/{{label}}rather than interpolating in a JS template literal, so the inline default is byte-identical to the value the pack serves.de-quote-pairing-3876.test.tsmoves 53 → 54 on both counts: the GermancloneCreatedadds one matched „…“ span around an interpolated hole, the same shapeflowRunner.completedcontributed.rdqstays 0, which is the half of that ratchet carrying the meaning.Verification — all at
082f466(branch merged up toorigin/main, no rebase)node scripts/check-i18n-call-site-keys.mjs— the gate that was redpnpm exec vitest run packages/i18n/(all-locales key parity + the de quote ratchet)pnpm exec vitest run packages/app-shell/src/views/setup/pnpm --filter @object-ui/i18n --filter @object-ui/app-shell type-checktsc --noEmit && tsc -p tsconfig.test.json)check:i18n-keys·check:i18n-drift·check:i18n-dead-keyspnpm exec eslintover the touched files,--format jsoncheck-changeset-presence·check-changeset-no-majormajor7306a81: app-shelltype-check, and vitest overviews/setup/+views/metadata-admin/previews/+views/metadata-admin/inspectors/(the new page plus every importer of the moved symbol)Reverse-verified — each leg mutated, the mutation confirmed on disk by anchor greps (never the editor's exit code), then restored:
_provenanceclause from the packaged-ness test → 3 red, incl. the cloud#970 counterexample and the rendered scoping case;actionErrorDetail→ the client's own fallback) → 3 red, exactly the three verbatim-refusal tests;Leg 3's first attempt was a no-op — a
perl s///whose pattern contained===and$failed to compile, leaving the file untouched, and the run came back green. The anchor greps caught it (injected 0, removed 1), so that green was discarded rather than reported; the leg was redone with a literal replace that exits non-zero on an anchor miss.Declared narrowing. The full 533-file
packages/app-shell/suite was started but held the shared verify lock 15 minutes with another agent queued behind it and produced no per-file progress, so it was stopped by its own PIDs (other agents run the same command from the shared checkout — apkill -fwould have taken out their runs) and replaced with the targeted runs above. CI runs the full farm.check:eager-closureis not reported: it requires a builtapps/console/dist/eager-closure.jsonand refuses locally as a broken gauge rather than reporting a false pass. It is the gate7306a81exists to keep honest — worth a look on CI.Generated by Claude Code