Skip to content

fix(app-shell): editing an action in Studio no longer stops the object from saving - #26827

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-kfmx2w
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-kfmx2w

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #26728
Fixes #26736

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 的动作编辑器(数据 › 对象 › 操作)三处修正:「开放给 AI 智能体」改写规范的嵌套 ai 块;切换「类型」时清掉新类型拒收的键(如 API 动作上的脚本 body);「请求体形态」改成「平铺 / 包裹(键)」控件。数据支柱对「只是还没填完」的动作暂存不发送并说明缺什么,对服务端拒收的保存在提示条里点名到具体动作,「定位」直接打开它。
  • 为什么改:这三处以前都会写出规范拒收的动作,之后对象的每次自动保存都被 422 拒绝,直到刷新页面,界面上也看不出是哪个动作出了问题。
  • 风险与回滚:低。不改规范、不新增元数据键、不碰服务端;只改设计器的写法、暂存判断与两种语言的文案。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx 的 patchAi / commitType / TYPE_SCOPED_KEYS;ui/packages/app-shell/src/views/studio-design/metadataError.ts 的 actionHeldInput(暂存判断,以规范的 ActionSchema 为准);StudioDesignSurface.tsx 的 showObjTarget。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/DataPillar.actionRefusal-26736.test.tsx,预期 2 个用例全绿;或在 Studio 里勾选「开放给 AI 智能体」再取消,对象照常保存,不再出现 422。
  • 需要你做的:无,席位落地。

What changes

Three controls in Studio's action inspector wrote an action @objectstack/spec refuses, and the Data pillar then sent it, so every later save of the object answered 422 until a reload:

control wrote now writes
Expose to AI agents, and its description flat aiExposed / aiDescription (refused by name) the spec's ai: { exposed, description }, merged into any ai block the draft holds; the next write clears the flat keys an older draft carries; unticking drops a description the spec would refuse as too short (its box hides while unexposed)
Type only type, keeping body, onSuccess, outcomeMessages, undoable: true the new type refuses type, plus undefined for each of those keys the draft holds and the new type refuses (TYPE_SCOPED_KEYS); everything the new type accepts, target included, is kept
Body Shape free text under "More fields" ({"wrap":"payload"} became a string; a stored { wrap } displayed as payload) a curated Flat / Wrap control with a key input: 'flat', or { wrap: KEY } once a key is typed (an empty key is never written; the spec would accept it and nest under "")

The refusal surface the inspector reports through (studio-design/metadataError.ts, the Data pillar in StudioDesignSurface.tsx, and ObjectActionsPanel.tsx):

  • Held, not refused. objectHeldEdit now also asks actionHeldInput of each action: the spec's ActionSchema judges the action, and when every issue is an input the inspector offers that has no value yet (a script source, a script's body when it has neither body nor target, the target a type requires, an exposed action's AI description absent or still too short) the object is held unsent, with the neutral line "Not saved yet: API endpoint on the action “Send” needs a value". Anything else is sent and its refusal shown.
  • A refusal is placed on the action. objectSaveRefusal gains objectActionLocator: an issue at actions.INDEX… names the action by its label, and the input when the key under it is one the inspector edits. "Show me" switches to the Actions view and selects that action (ObjectActionsPanel's new focus prop), from any view.
  • Recovery needs no reload. The autosave already resends on the next edit; with the writes above, that edit leaves a draft the spec accepts, and the strip clears when the save lands.

ActionPreview read the flat aiExposed for its AI pill; it now reads ai.exposed, since the inspector no longer writes the flat key (bounded in-place fix, on the claim's amended surface). New studio namespace strings, en and zh: the Body Shape control, engine.studio.refusal.action / actionInput, engine.studio.held.actionNeedsInput. No packages/spec/**, server or new metadata key.

Why

One visible checkbox, or an ordinary Type switch, stopped the whole object from saving, with nothing on screen saying which action or why; a reload discarded the edits. The spec already defines every shape written here, so the defect was the designer's.

Tests

  • ActionDefaultInspector.aiBlock-26728.test.tsx (ask 1): the real inspector, drafts parsed with ActionSchema after a JSON round trip. A tick writes { ai: { exposed: true } } and the only refusal is ai.description; a 40-character description parses and reads back; unticking parses with and without a description; a draft carrying the flat keys parses after the control's next write; other ai keys are kept; AI_DESCRIPTION_MIN is the spec's minimum both sides.
  • ActionDefaultInspector.typeSwitch-26736.test.tsx (asks 2, 3): script with a body → API drops the body, and the draft parses once the endpoint is typed (and after a Method edit); every type-scoped key is cleared, undoable: false kept; a switch with nothing to clear writes the type alone and keeps target. Body Shape: a stored { wrap: 'payload' } reads back as Wrap / payload; Wrap writes nothing until a key, then { wrap: KEY }; clearing the key takes the shape off; Flat writes 'flat'; the old JSON text is refused invalid_union. Parity: each TYPE_SCOPED_KEYS entry parsed on every ActionType, refused exactly off its listed types.
  • metadataError.actionHeld-26736.test.ts (asks 2, 4): seven held cases, each paired with the spec's own refusal; four finished-but-wrong cases not held; the object-level hold names the action and selects it; field holds still come first; refusals at actions.N.body and actions.N are placed on the action; field paths unchanged; an unplaceable path stays unlocated.
  • DataPillar.actionRefusal-26736.test.tsx (asks 1, 2, 4): the real Data pillar, real inspector, real MetadataClient, a transport that parses each action with ActionSchema and answers 422 with actions.N… issues. A refused save names the action; "Show me" from the Form view opens it on Actions; tick + untick sends an accepted save and the strip clears, no reload. Script → API is held (no PUT, held line names the endpoint on the action), and typing it sends an accepted save.
  • Moved fixtures: automationPreviews.i18n-10862.test.tsx and inspectorStrings.hookAction.i18n-10586.test.tsx now carry ai: { exposed } instead of the flat keys.
  • Ablations (one-off, not kept), each through node scripts/ablation-replace.mjs on committed head 27b93089, each restored to the HEAD blob with git diff HEAD empty: A1 patchAi writes aiExposed → aiBlock 4 failed; A2 Type switch clears nothing → typeSwitch 2 failed + DataPillar held case failed; A3 no action hold → metadataError 1 + DataPillar 1 failed; A4 no action locator → metadataError 2 + DataPillar refusal case failed. Control after restore: Tests 36 passed (36).

Acceptance notes

  • ai stays editable under "More fields" (not curated), so category, paramHints, outputSchema and requiresConfirmation remain reachable; exposed and description are therefore editable in both places, writing one block.
  • A Type switch keeps target. Moving between two target types (a URL to a flow) keeps the old value in the new type's picker, where an unknown name shows as (not found); the spec judges it on the new type.
  • The hold's notion of "unfinished" is the spec's own refusal at those inputs, so a whitespace-only script source, which the spec accepts, is sent.
  • StudioDesignSurface.tsx edit is about 20 lines in the Data pillar; Studio Automations: with the right panel hidden, selecting a node shows its configuration nowhere — the config drawer only reopens on mobile #26737 edits the same file's flow fold, so whichever lands second merges main.

Verification

All readings at head 27b93089 (base c8583f80), under scripts/pm/os-verify-lock.sh where heavy.

Touched packages: @object-ui/app-shell, @object-ui/i18n
  • Prerequisite builds: pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/sdui-parser --filter=@objectstack/lint --filter=@objectstack/client --concurrency=2, 36 tasks, VERDICT command-exit 0.
  • Type-check: cd ui && pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2, 65 tasks; both packages run tsc --noEmit && tsc -p tsconfig.test.json (the test config covers the new tests); VERDICT command-exit 0.
  • The four new test files plus every ActionDefaultInspector*, metadataError*, DataPillar*, ObjectActionsPanel* file and the two moved fixtures: Test Files 41 passed (41), Tests 322 passed (322). The rest of app-shell is left to CI's Test UI shards.
  • pnpm --dir ui exec vitest run scripts/ (reads the i18n catalogues): Test Files 178 passed, 2 skipped (180); Tests 5065 passed, 2 skipped; VERDICT command-exit 0.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 commands, each run with its exit code captured before any pipe: 34 exit 0. PR-body gates read this body through GITHUB_EVENT_PATH / PR_BODY.
  • dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.

…t from saving

The action inspector writes the spec's nested `ai` block instead of the
flat `aiExposed` / `aiDescription` it refuses, clears the keys a Type
switch makes refused, and edits Body Shape as Flat or Wrap(key). The Data
pillar holds an action that is only unfinished, and places a refused one
on the action, with "Show me" opening it on the Actions view.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSHgVcT66bTgBM8Yq3
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T18:24Z · head f4c6409c9fc43892b72461258b3cdd8edd2fa4b0

  1. Shape: draft, base main, closes both cards: Fixes #26728 and Fixes #26736. Clause-②: no and Metadata-protocol: none match the claims (6448125382, 6448164255) and their amendments (6448278843, 6448298432), in the body and in the changeset (@object-ui/app-shell and @object-ui/i18n patch). 14 files, all inside the amended surface: ActionDefaultInspector.tsx, studio-design/metadataError.ts, StudioDesignSurface.tsx (the Data pillar's "Show me" routing only, 17 lines), ObjectActionsPanel.tsx (a focus prop), ActionPreview.tsx (AI pill reads ai.exposed) with its two moved fixtures, studio namespace en.ts/zh.ts (8 keys each), four new test files, one root changeset. No packages/spec/**, server or new metadata key.

  2. Premise, measured: the seat read the diff.

  3. Gates, pinned to the head: 52 check-runs on the head: 43 success, 9 skipped, 0 failure, 0 pending. · check-governed-merges --pr 26827: NOT governed, 0 of 14 paths hit the register (1098 changed lines, under the human-merge threshold). mergeable_state: clean.

  4. Accepted deviations:

    • the surface grew as amended (metadataError.ts, the Data pillar's routing in StudioDesignSurface.tsx, ObjectActionsPanel.tsx, ActionPreview.tsx plus two fixtures); metadataError.ts imports TARGET_INPUT_KEYS from actionPresets.ts without changing it;
    • the local app-shell run was narrowed to 41 related files (322 tests); CI's Test UI shards are the reading.
  5. Out of scope: two observations with no carrier: ai is also editable under "More fields"; a Type switch keeps target across target types (by design here, and the spec judges its value on the new type). Studio Data › Actions: "Add field value" adds no row, so an update action's patch cannot be authored in Studio (the published action is a no-op) #26735 and Studio Data › Actions: a "Run a script" action is saved without type, so the console treats it as not executable — clicking it sends no request #26734 (same inspector) are dispatched after this lands. Skills-docs: is not owed.

  6. Prose face, checked sentence by sentence against the diff: each changeset claim matches a hunk and a test:

    • AI tick writes ai.exposed; untick parses; a flat-key draft is cleaned by the next write; the preview pill reads the block;
    • a Type switch clears what the new type refuses and keeps the rest;
    • Body Shape round-trips { wrap }, writes Wrap only with a key, Flat as 'flat';
    • an unfinished action holds the save with a neutral line naming the input; a refused save is placed on the action, "Show me" opens it, and the next accepted save clears it.

    Four ablations went red and were restored; control 36/36.

Landing: no contract review is owed (no published schema, no governed text, Clause-②: no). The seat marks the PR ready and arms auto-merge. On MERGED, Fixes #26728 and Fixes #26736 close both cards.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 18:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit e9e344a Oct 27, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kfmx2w branch October 27, 2026 18:57
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40562512172 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (3/16) — 失败步骤: Run the ui tests (shard 3/16)

    × lists every key the spec ActionSchema declares 19ms
    FAIL   unit  packages/core/src/actions/__tests__/actionKeys.pin.test.ts > action key inventory (objectstack#4075 step 1) > lists every key the spec ActionSchema declares
      ↳ 失败原因: AssertionError: expected { missing: [ 'disabledReason' ], …(1) } to deeply equal { missing: [], stale: [] }
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 10 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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