You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit eca7cd2
Browse filesBrowse the repository at this point in the historyBrowse files
authored
feat(chatbot): confirm card for one record written from AI chat (cloud#3565) (#30058)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} -->
_Requested by **Sam** · [project
thread](https://claude.ai/code/project/chan_01GsBogozeZG5sdpAARrN7HP?thread=cmsg_01GsBogozeZG5sdpAARrN7HP5qCsohzeY1qHxN6c2Swhw2)_
Refs objectstack-ai/cloud#3565 — the framework half of the ruling
(maintainer, 2026-11-04): the AI chat creates ONE record or changes ONE
record after the person confirms on a card. The cloud half is
objectstack-ai/cloud#3574 (the `create_record` / `update_record` /
`remove_sample_data` tools) and objectstack-ai/cloud#3572 (the runtime
cleanup service); cloud's tool-name conformance test goes green when a
pin round carries this PR.
## What changes
- **spec** — `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']`:
−`draft_record`, +`create_record`, +`update_record`,
+`remove_sample_data`. The lint test for AI tool references follows the
rename (`create_record` / `update_record` resolve; `create_records` and
`draft_record` are reported).
- **plugin-chatbot** — `detectRecordWrite` and `detectSampleDataCleanup`
read the pending-action payload the cloud tools attach; the live mapper
lifts `recordWrite` / `sampleDataCleanup` onto the tool invocation.
`ChatbotEnhanced` draws the record-write card in place of the generic
approval strip: the fields to write (stored → proposed on an update),
the dropped fields with their reason, and the three fixed buttons —
**Confirm** (approve → the write runs), **Open full form** (reject with
a declared reason, then the host opens the form), **Cancel** (reject
with a declared reason). After Confirm the card links the written
record; after Cancel it says so. A `sampleDataCleanup` proposal draws
the seeded tables and counts above the generic Approve / Deny strip.
`useHitlInChat` now records which button was pressed (`decision`) and
the executed `result`, so the card draws its own outcome instead of
parsing the English status line.
- **app-shell** — `AiChatPage` wires the card: resolves the record's app
from `/api/v1/meta/object/<name>` (`_packageId`, else `setup`), opens
`/apps/<app>/<object>/new?<prefill>` for a create
(`recordFormPrefillQuery`: scalars as-is, lists joined, objects as JSON)
and the record's `/edit` page for an update, and rehydrates the card
from stored messages.
- **i18n** —
`chatbot.tool.{create_record,update_record,remove_sample_data}` and
`console.ai.recordWrite*` /
`sampleDataCleanup{Title,Irreversible,Absent,Edited}` in all ten
locales.
Known gap, on purpose: for an **update**, Open full form lands on the
plain edit page without the proposed values. `ObjectForm` folds
`defaults` into `initialValues` regardless of mode, so a prefilled edit
overlay would overwrite stored values the person did not change; a
create is prefilled. Filed in the cloud report.
## Why
Ruled on cloud#3565: both agents write as the person under their
permissions; the model never passes ids the person did not see; the
confirm card is the only way a write happens; delete and bulk stay out
of chat (the agent opens the record instead). This PR is the console's
side of that contract — the card and the tool names — so cloud can
register the tools against the spec registry.
## Revised 2026-11-04 after seat 2's review (head 37bfbad)
- **The 「清理示例数据」 card says the rows cannot be recovered**, on every
card: `sampleDataCleanupIrreversibleLabel` (default "Removed rows cannot
be recovered."), wired from `console.ai.sampleDataCleanupIrreversible`
(「删除后无法恢复」) in all ten locales — the maintainer's dependency note on
cloud#3565 until the recycle bin (objectstack#3146).
- **`absent` is rendered** whenever it is above 0
(`sampleDataCleanupAbsentLabel(count)`,
`console.ai.sampleDataCleanupAbsent`), and so is the new `edited` count
— the recorded rows kept because someone edited them since the replay
(`sampleDataCleanupEditedLabel(count)`,
`console.ai.sampleDataCleanupEdited`; `detectSampleDataCleanup` reads
`edited` as 0 when an envelope has none). Both are i18next count
families with every plural slot their language selects (objectui#11445).
Two card cases pin the three lines.
- **The spec changeset names the `draft_record` retirement's reach:** no
skill in this repo names it (the lint test names it only as its control;
two spec page fixtures called `draft_record` are page names), the one
skill anywhere is the cloud chat's `record_drafting` skill, moved in
cloud#3574; a third-party skill still naming it reports
`ai-skill-tool-unresolved`. Still `patch`. The plugin-chatbot and i18n
changesets name the new props and keys.
## Verification
From `ui/` (the vitest guard requires the ui root):
```
pnpm exec vitest run packages/plugin-chatbot/src/__tests__/recordWrite-3565.test.ts \
packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.recordWriteCard-3565.test.tsx \
packages/plugin-chatbot/src/__tests__/useHitlInChat.test.tsx \
packages/app-shell/src/console/ai/__tests__/recordFormPrefill-3565.test.ts \
packages/app-shell/src/console/ai/__tests__/AiChatPage.recordWriteRehydration-3565.test.tsx \
packages/i18n
pnpm exec vitest run packages/plugin-chatbot packages/app-shell packages/i18n # 239 files, 2598 passed, 13 skipped
pnpm --filter @object-ui/plugin-chatbot exec tsc --noEmit -p tsconfig.json
pnpm --filter @object-ui/app-shell exec tsc --noEmit -p tsconfig.json
```
From the repo root: `pnpm --filter @objectstack/spec build && pnpm
--filter @objectstack/spec check:generated --fix` (no generated file
changed), `pnpm --filter @objectstack/lint exec vitest run
src/validate-ai-tool-references.test.ts`, `pnpm lint` on the changed
files (0 errors; the warnings are the file-level fast-refresh class
every export in `plugin-chatbot/src/index.tsx` already carries), `pnpm
check:i18n`, `check:i18n-coverage`, `check:i18n-walk-parity`,
`check:i18n-stale-fill`, `check:empty-changeset`,
`check:changeset-fixed`, `check-changeset-no-major`.
Clause-②: no
Metadata-protocol: none
_Keep one value on each of the two lines above, each line starting with
its key.
[Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum)
item 5 says what they mean._
## Human Minimum
Each item is one rule from
[CONTRIBUTING.md › Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum),
which links the AGENTS.md text that states it.
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QUs3BvspJfE7QBDwL7tH8d
<!-- signed: repo:cloud#2 · os-zhuang · session_01QUs3BvspJfE7QBDwL7tH8d
-->
---------
Co-authored-by: os-zhuang <sam@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
feat(i18n): `chatbot.tool.create_record` / `update_record` / `remove_sample_data` labels and the record-write card's `console.ai.recordWrite*` labels in all ten packs; `chatbot.tool.draft_record` retired
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The spec registry now lists the cloud chat agents' confirmed record write and the sample-data cleanup (cloud#3565) and drops `draft_record`, so the chatbot's tool timeline gets a sentence for each new step in every pack ("Create the record" / "新建记录", "Change the record" / "修改记录", "Remove the sample data" / "清理示例数据") and loses the retired one, keeping `toolLabels-locale-parity-7481` and `toolLabels-sentences-12724` green. The console's record-write card gets its eleven labels under `console.ai.recordWrite*`: the create and update titles (with the object's label), the three buttons 确认 / 打开完整表单 / 取消, the two stored reasons, the "not included" lead-in and the three states (writing, written, cancelled) — plus the 「清理示例数据」 card's four: `console.ai.sampleDataCleanupTitle` (its title over the seeded tables and counts), `console.ai.sampleDataCleanupIrreversible` (「删除后无法恢复」 — the removal cannot be undone, said on every card), and the two count families `console.ai.sampleDataCleanupAbsent` (the recorded rows the card does not list) and `console.ai.sampleDataCleanupEdited` (the rows kept because someone edited them), each with every plural slot its language selects (objectui#11445).
feat(plugin-chatbot): the record-write card — ONE record the chat proposes and the user confirms, with 确认 / 打开完整表单 / 取消 (cloud#3565)
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The cloud chat's `create_record` / `update_record` tools queue a HITL pending action in the user's name and answer the approval envelope plus the record: `{ status:'pending_approval', pendingActionId, recordWrite:{ operation, objectName, objectLabel, recordId?, label?, reason?, fields:[{ name, label, type, value, current? }], dropped } }`. The chat now draws that as a card instead of the generic Approve / Deny strip and the raw payload:
11
+
12
+
-**New export**`detectRecordWrite(result)` (with the `RecordWrite` and `RecordWriteField` types), lifted onto `ChatbotEnhancedToolInvocation.recordWrite` by the live mapper beside `pendingActionId`. It reads the bare object, the persisted `{ type:'text', value }` wrapper and a JSON string; an envelope without the pending id or without a field to confirm is not a card.
13
+
-**The card** (`data-testid="record-write"`): the create or update title with the object's label, the record's label and reason, every field as a labelled row — an update shows the stored value struck through, then →, then the proposed one — the fields the server left out and why, and three buttons. 确认 calls `onToolApprove(toolCallId, true)` (the pending action's dispatcher writes as the user); 取消 calls it with `false` and `recordWriteCancelReason`; 打开完整表单 calls it with `false` and `recordWriteOpenFormReason` FIRST, then the new `onOpenRecordForm(write)` prop, so a record saved in the form is never written a second time from the card (disabled where the host passes no handler). After a decision the card says written (with the record's link through `onOpenRecord`, read from the dispatcher's `{ status:'written', objectName, recordId, label }` result), cancelled, or the server's refusal.
14
+
-**New props**: `onOpenRecordForm`, `recordWriteCreateTitle(objectLabel)`, `recordWriteUpdateTitle(objectLabel)`, `recordWriteConfirmLabel`, `recordWriteOpenFormLabel`, `recordWriteCancelLabel`, `recordWriteCancelReason`, `recordWriteOpenFormReason`, `recordWriteDroppedLabel`, `recordWriteSubmittingLabel`, `recordWriteWrittenLabel`, `recordWriteCancelledLabel`, all with English defaults.
15
+
-**「清理示例数据」** rides the same seam: `detectSampleDataCleanup(result)` (with the `SampleDataCleanup` type) lifts `{ status:'pending_approval', pendingActionId, sampleDataCleanup:{ tables:[{ objectName, label, count }], total, absent, edited } }` onto `sampleDataCleanup`, and the chat draws the tables and counts (`data-testid="sample-data-cleanup"`, title from the new `sampleDataCleanupTitleLabel` prop) above the generic Approve / Deny strip, with the raw payload hidden. The card always says the removal cannot be undone (`sampleDataCleanupIrreversibleLabel`, default "Removed rows cannot be recovered." — there is no recycle bin until objectstack#3146), says how many recorded rows it does not list when `absent` is above 0 (`sampleDataCleanupAbsentLabel(count)`) and how many stay because someone edited them when `edited` is above 0 (`sampleDataCleanupEditedLabel(count)`; an envelope without `edited` reads as 0).
16
+
-**`ToolDecisionState`** (widened, additive): every state may carry `decision: 'approved' | 'rejected'`, and an approved `success` may carry `result`, the dispatcher's answer. `useHitlInChat` sets both; the generic strip keeps reading `message` alone.
feat(spec): `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` lists `create_record`, `update_record` and `remove_sample_data`; `draft_record` is retired
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The cloud chat agents now create or change ONE record of the user's data after the user confirms it on a card, and remove the sample rows a seed replay recorded after a card listing them (cloud#3565, the maintainer's ruling of 2026-11-04). The three tools replace `draft_record` (cloud#3383), whose "draft it and the user saves it in a form" ending was never built. The registry lists the three names in their alphabetical order and drops the retired one, so `PLATFORM_PROVIDED_TOOL_NAMES` carries them and a skill whose `tools[]` names them resolves under ADR-0109 reference integrity, while a skill still naming `draft_record` is reported as a fictional tool (`ai-skill-tool-unresolved`). That is the whole change: three accepted `skill.tools[]` values in, one out. No schema shape changes, no export is added or removed.
11
+
12
+
**Reach of the retirement.**`draft_record` was a published constant, so removing it breaks any skill whose `tools[]` still names it. Measured before this landed: no skill in this repo names it (`validate-ai-tool-references.test.ts` names it only as the control proving the report fires; the two spec page fixtures called `draft_record` are page names, not tools); the one skill anywhere that names it is the cloud chat's `record_drafting` skill, which the cloud half of cloud#3565 (cloud#3574) moves to `create_record` / `update_record` in the same landing. A third-party skill that still names it reports `ai-skill-tool-unresolved` at validation and is not silently dropped. That is why this is graded `patch` and not left as a silent one.
0 commit comments