Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

feat(core,react,app-shell): action confirmText, successMessage and errorMessage read the template envelope and fill {{ }} holes (objectui#13367) - #13409

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-13367-action-copy-template
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-13367-action-copy-template

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #13367

The renderer half of objectstack-ai/objectstack#24814 (ruling A): an action's confirmText, successMessage and errorMessage read the template envelope and fill {{ }} holes, before the spec types the three slots as template slots. Claim: #13367 comment 6369803045.

What changes

  • ActionRunner (@object-ui/core) reads each key as a string or { dialect?: 'template', source? }:
    • confirmText: {{ }} holes over record (the record the action runs against; on a list, the clicked row, the same rule as templateEvaluatorFor) and params (the action's static params object, without the stashed row; the collected values do not exist yet when the confirmation shows);
    • successMessage: {{ }} holes over result (the payload ${result.*} reads) and record;
    • errorMessage: no root is declared yet, so the text shows as written.
  • The engine is @objectstack/formula's templateEngine, the one the flow notifications render with and the one list bulk action confirmText: fill the template slot's declared holes — {{count}} first, the action's params second (the renderer half of objectstack#24798 ruling B) objectstack#25942's ruling names. It is exported at this repo's .objectstack-sha pin and already a dependency of @object-ui/core. No engine and no hole grammar is written here.
  • Transition: a sentence with no {{ }} hole keeps today's reading (${…} through ExpressionEvaluator for confirmText, ${result.*} for successMessage), so the platform objects' ${result.email} sentences keep working until the spec card converts them.
  • One dialect per sentence: a sentence with a {{ }} hole goes through the template engine only. A record value that happens to contain ${…} is never evaluated after it was filled into a hole.
  • No object reaches a React child: an envelope with no string source gives '' to the confirm handler (the declared confirmation still gates the run), the default text to the success toast, and the action's own error to the error toast.
  • useActionTextLocalizer (@object-ui/react) kept the three keys as I18nLabel maps and collapsed them with pickLocalized, which turns { dialect: 'template', source } into its first string value: the word template. It now hands the envelope on, and a bundle entry replaces its source (i18next leaves a missing {{ }} variable in place, measured on the installed version).
  • Flow actions (@object-ui/app-shell): flowSuccessCopy read the invoking action's successMessage only as a string, so an envelope fell to the flow's own copy. It now reads it through actionCopySource. FlowRunner's actionSuccessMessage takes the same two shapes (the type widening made app-shell's type-check red at the two launch sites otherwise).
  • @object-ui/core exports the reader: ActionCopyTemplate, isActionCopyEnvelope, actionCopySource, hasTemplateHoles, fillTemplateHoles.

Clause-②: yes (the claim said no; ActionDef's three keys widen and core gains five exports, so this declares the widening)
Metadata-protocol: none

Tests

  • New packages/core/src/actions/__tests__/ActionRunner.copyTemplate-13367.test.ts (28): a plain string and an envelope per key; each root (record from the header and from a row, params without _rowRecord, result); a formatter; an undeclared root prints empty; ${…} still filled for confirmText and successMessage; one dialect per sentence for both; a refused source shown as written; an envelope with no source for each key; outcomeMessages still outranks; an envelope counts as declared copy for a navigation (objectui#12755); the reader helpers.
  • New packages/react/src/hooks/__tests__/useActionTextLocalizer.templateEnvelope-13367.test.tsx (5): nameless and named actions keep the envelope, a bundle entry replaces source, a sourceless envelope passes through, strings and maps unchanged.
  • packages/app-shell/src/utils/__tests__/flowResponse.test.ts: 3 new cases for the envelope on a flow action.
  • Ablation (on f25bc24): the localizer's collapseCopy put back to pickLocalized, and the success rung put back to string-only: 9 failed (5 runner, 4 localizer); restored with git checkout HEAD -- packages, git diff --quiet HEAD clean.
  • pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ → 331 files, 5566 passed. packages/components/src/renderers/action/ packages/app-shell/src/hooks/__tests__/ packages/app-shell/src/utils/ packages/plugin-grid/ → 391 files, 3995 passed. After the app-shell commit: app-shell hooks, utils and flow views → 141 files, 1427 passed. After merging origin/main (889d028): core actions, react hooks, flowResponse and the objectui#13379 navigator pins → 84 files, 1097 passed.
  • NODE_OPTIONS=--max-old-space-size=8192 pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/console → 37/37. A first run over app-shell, console, components and the plugin packages went red on the two launch sites above, which the second commit fixes.
  • eslint on the touched files: 0 errors. Gates exit 0: check:new-line-citations, check:control-bytes, check:spec-symbols, check:changeset-claims, check:pending-changeset-literals, check:unreferenced-sources, check:test-path-roots, check:action-forward-parity, check:handler-key-reads, check:phantom-deps, check:self-import, check:esm-specifiers, check:component-surface-parity, check:vi-mock-specifiers, check:comment-mask-corpus, scripts/check-changeset-presence.mjs. check:readme-exports needs every package built and was left to CI.

Order

This PR → objectstack's .objectui-sha moves past it → objectstack-ai/objectstack#24814 types the slots (it carries Blocked-by: objectstack-ai/objectui#13367).

Serial

objectui#13395 (objectui#13384) also edits ActionRunner.ts, in other hunks (a refusal inserted just above the confirm read, and the RUNNER_TEXT docblock, whose last sentence this PR rewords). Whichever lands second merges main.

Acceptance notes

  • {{ }} formatters render in en-US: the runner has no language (its docblocks say so), so {{ amount | currency }} does not follow the user's locale yet. Noted, not filed.

  • FlowRunner's terminal toast after a paused flow shows the action's copy as written: neither {{ }} nor ${result.*} is filled there, before or after this PR. Noted, not filed.

  • The Studio action inspector and preview (ActionDefaultInspector, ActionPreview) read the three keys as I18nLabel maps, so an envelope would show there as template. Authoring surface, outside the runner; noted, not filed.

  • list bulk action confirmText: fill the template slot's declared holes — {{count}} first, the action's params second (the renderer half of objectstack#24798 ruling B) objectstack#25942 (bulk confirmText fill step, baozhoutao) can call the same fillTemplateHoles over { count, … }; this PR does not touch plugin-grid.

  • Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0159TkE3rFbrKwMs3mWhnniW

…{ }} holes

ActionRunner reads confirmText, successMessage and errorMessage as a
string or a { dialect: 'template', source } envelope, and fills {{ }}
holes through @objectstack/formula's templateEngine: confirmText over
record and params, successMessage over result and record, errorMessage
verbatim. A sentence with no {{ }} hole keeps the ${...} reading.
useActionTextLocalizer keeps the envelope instead of collapsing it to
its first string value.

Claude-Session: https://claude.ai/code/session_0159TkE3rFbrKwMs3mWhnniW
Co-authored-by: Claude <noreply@anthropic.com>
flowSuccessCopy read the invoking action's successMessage only as a
string, so an envelope fell to the flow's own copy. It now reads it
through core's actionCopySource, and FlowRunner's actionSuccessMessage
takes the same two shapes as ActionDef.successMessage.

Claude-Session: https://claude.ai/code/session_0159TkE3rFbrKwMs3mWhnniW
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 77 chunks) 3243.3 KB 3744.1 KB
Main entry chunk (gzip) 81.8 KB 350 KB
Entry file index-j2pLrUFu.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 (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.41KB 7.90KB
app-shell (runtime-config.js) 24.52KB 8.48KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 614.12KB 150.38KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 253.04KB 70.68KB
fields (index.js) 295.17KB 75.63KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.17KB 11.96KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (ServerReadScope.js) 4.03KB 1.82KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.99KB 0.43KB
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) 16.01KB 3.91KB
plugin-calendar (index.js) 55.41KB 16.19KB
plugin-charts (index.js) 85.95KB 23.65KB
plugin-chatbot (index.js) 237.89KB 56.23KB
plugin-dashboard (index.js) 147.15KB 39.95KB
plugin-designer (index.js) 232.83KB 50.01KB
plugin-detail (index.js) 267.21KB 71.14KB
plugin-editor (index.js) 2.21KB 1.04KB
plugin-form (index.js) 185.46KB 48.48KB
plugin-gantt (index.js) 178.19KB 45.06KB
plugin-grid (index.js) 278.21KB 77.54KB
plugin-kanban (index.js) 64.27KB 19.81KB
plugin-list (index.js) 149.54KB 39.24KB
plugin-map (index.js) 27.11KB 8.99KB
plugin-markdown (index.js) 13.79KB 4.79KB
plugin-report (index.js) 46.42KB 13.06KB
plugin-timeline (index.js) 39.30KB 11.87KB
plugin-tree (index.js) 15.25KB 5.38KB
plugin-view (index.js) 94.26KB 24.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 129.13KB 42.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 4f05caef3c71785e0a626cf63e150ab03a5f7f20
Local-runs: probe — scripts/check-governed-queue-guard.mjs --test over the committed file list, to read the face (NOT GOVERNED, 10 paths)

Face: the Clause-②: yes declaration alone (no published schema, no governed path), so the lane seat reviews at TIER_DEFAULT. Inputs read: the card objectui#13367 (the renderer half of objectstack-ai/objectstack#24814, ruling A), its claim 6369803045 and os-dev-report 6370584499, and the PR diff at this head. Check conclusions are taken from this head's check-runs.

① Derived judgments

The diff widens one public interface and adds one public module. No existing member is narrowed.

  1. ActionDef.confirmText, .successMessage and .errorMessage change from string to ActionCopyTemplate (string | { dialect?: 'template'; source?: string }). Every value that type-checked before still does.
    • The runner reads both arms through one reader, actionCopySource.
    • A sentence with a {{ }} hole is filled by @objectstack/formula's templateEngine. That is the engine the flow notifications use, and the one objectui#13183 names. No second hole grammar is added.
    • A sentence with no hole keeps today's ${…} reading, so the platform's ${result.email}-style sentences keep working until the spec card converts them.
    • Each sentence is read in exactly one dialect, so a value filled into a {{ }} hole is never re-read as ${…}.
    • The roots match the card's ruling: confirmText gets record and static params (the dialog's values do not exist yet); successMessage gets result and record; errorMessage gets none and is shown as written.
    • Correct.
  2. A confirmText envelope with no text still asks for confirmation, with an empty body. Dropping it would let a destructive action run unasked, and an object never reaches the dialog or a toast (the React Task: Refactor Monorepo to Support Lazy-Loaded Plugins #31 guard). An empty-string confirmText still asks nothing, as before. Correct.
  3. @object-ui/core exports ActionCopyTemplate, isActionCopyEnvelope, actionCopySource, hasTemplateHoles and fillTemplateHoles, which @object-ui/react and @object-ui/app-shell read.
    • isActionCopyEnvelope can never take an I18nLabel locale map for an envelope, since dialect and source are not language tags.
    • fillTemplateHoles returns the source as written when the engine refuses it, so the user reads the author's sentence rather than nothing.
    • Correct.
  4. useActionTextLocalizer keeps the envelope's shape. Read as a locale map, an envelope collapsed to the word template. A bundle entry replaces only source.
    • flowSuccessCopy reads the envelope through the same reader, so an action's own success copy still outranks the flow's. ScreenFlowState.actionSuccessMessage widens to match.
    • Correct.

@object-ui/core already depended on @objectstack/formula (package.json, ActionEngine), so no dependency is added.

② Semver level

The changeset declares minor for @object-ui/core (three widened members and five new exports) and patch for @object-ui/react and @object-ui/app-shell. That matches the diff, and no major is declared.

③ Boundary flags

Implemented-by: claude/issue-13367-action-copy-template
Reviewed-by: session_01WJozybDmRH8z2c5SD7vJkj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT — objectui#13367 (p3, M, card thread), head 4f05caef3c71785e0a626cf63e150ab03a5f7f20, reviewed 2026-10-23T17:19Z.

Seat: domain:ui#1 · Clause-②: yes(合同复核见本 PR 上的 ## Contract review,VERDICT: PASS)

项 读数
形态 10 个文件,+652/−31:@object-ui/core 的 ActionRunner.ts、新模块 actionCopyTemplate.ts、actions/index.ts 和测试;@object-ui/react 的 useActionTextLocalizer.ts 和测试;@object-ui/app-shell 的 flowResponse.ts(含测试)、FlowRunner.tsx;一个 changeset(core minor,react、app-shell patch)。NOT GOVERNED。与当前 main 无冲突(本次读取时 git merge-tree 无冲突)。
改动核对 读了 diff。confirmText、successMessage、errorMessage 既读字符串也读 template 信封;带 {{ }} 的句子由 @objectstack/formula 的 templateEngine 填空(核心早已依赖它),不带的保留 ${…} 写法,每句只按一种写法读。填空的根:confirmText 为 record 和静态 params,successMessage 为 result 和 record,errorMessage 原样显示。没有文字的 confirmText 信封仍要求确认,正文为空,对象不会进入对话框或提示。useActionTextLocalizer 保留信封形状,语言包条目只替换 source;流程动作的成功文案经同一读取器,动作自己的文案仍优先于流程的。
卡线程提请 1)Clause-② 从认领的 no 改为 yes:三个键放宽、core 新增 5 个导出,PR 正文和 changeset 已声明,合同复核 PASS。2)文件面多了 flowResponse.ts 和 FlowRunner.tsx:类型放宽让 app-shell 的两处流程启动点类型检查变红,不改的话信封会输给流程自己的文案,接受,本条记录作为文件面的补充。
门禁 卡线程报告 CI 本 head 全绿;合并前由席位脚本再读一次 check-runs。
串行 PR #13395(objectui#13384)也改 ActionRunner.ts 的邻近位置,后落地的一方合并 main。objectui#13183(baozhoutao 认领)只是共用同一引擎,未触及其代码。
系列 spec 一半 objectstack#24814 在 .objectui-sha 推进到含本 PR 的提交后再给这三个槽位定类型。
落地条款 PR 正文 Fixes objectstack-ai/objectui#13367:合并后关卡。

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

2 participants