Skip to content

Commit d12c615

Browse files
os-zhuangclaude
andauthored
feat(plugin-chatbot): the plan card says how the build will land (objectui#12724) (#13508)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} --> _Requested by **Sam** · [project thread](https://claude.ai/code/project/chan_01GsBogozeZG5sdpAARrN7HP?thread=cmsg_01GsBogozeZG5sdpAARrN7HPPdgXnLU8SfcQmac3AzRouL)_ Clause-②: yes (the exported `ProposedPlanCard` gains the optional `posture`) Closes #12724 ## What objectstack-ai/cloud declared `planCard.posture` on `propose_blueprint`'s envelope (cloud#2924 ruling A, cloud PR #3111, `f0ecef3a`): `{ mode: 'publish' | 'draft', liveRecords, line }`. objectui drew nothing from it yet. The plan card now draws `posture.line` (for example 「首次搭建直接上线;之后的修改先存草稿,由你发布」) as the **last item of the assumptions list**, where the AiBuild artboard draws it. A plan card without the key draws as before. If a plan has a posture but no assumptions, the section still renders and holds the posture line alone. - `mapMessages.ts`: `ProposedPlanCard.posture?: { mode; line }`, lifted in `liftPlanCard` only when `mode` is a declared value and `line` is non-empty. `liveRecords` is already in the line, so it is not lifted. - `ChatbotEnhanced.tsx`: one `<li data-testid="proposed-plan-posture" data-posture={mode}>` after the assumptions. - `fixtures/build-cards.schema.ts`: re-vendored verbatim at cloud `f0ecef3a`. The only change from the `f7ab9cc8` copy is `planCard.posture`, and cloud `main` `36b07fdf` has the same schema byte for byte. - `buildCards.fixtures.ts`: `planEnvelope(lang, { posture: 'firstBuild' | 'draft' })`, built with cloud's own `postureFirstBuild` / `postureIntoLiveApp` wording. - The docs paragraph in `content/docs/plugins/plugin-chatbot.mdx`, and a changeset (`@object-ui/plugin-chatbot` minor). ## Why this closes #12724 The card's two remaining artboard rows have no producer key at cloud `main` `36b07fdf`, and no cloud card plans one: - the plan card's 「确认后约 1 分钟搭好」 and 「也可以直接回复:…」 - the result card's subtitle 「应用已进启动台 · 包 … · 以下每一条都按方案核对过」 The maintainer ruled in the project thread on 2026-10-25 to drop both from the artboard rather than wait for cloud. This posture row was the last declared key still undrawn. ## Tests - `pnpm exec vitest run packages/plugin-chatbot/`: 83 files, 795 tests, all passed, including the new `ChatbotEnhanced.planCardPosture-12724.test.tsx`. It covers zh and en first-build lines, the draft line, posture without assumptions, and two controls: no key, and an undeclared mode. - `buildCards.contract-12724.test.ts` checks both new fixture payloads against the vendored schema. - `node scripts/check-changeset-presence.mjs` ✅. `pnpm check:new-line-citations`: 0 new. - [x] Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer) --- _Generated by [Claude Code](https://claude.ai/code/session_01R3ARF6ZEyBWW8EBT9we3eX)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0063f22 commit d12c615

8 files changed

Lines changed: 168 additions & 5 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@object-ui/plugin-chatbot": minor
3+
---
4+
5+
Clause-②: yes
6+
Metadata-protocol: none
7+
8+
The AI build chat's plan card now says how the build will land. When `propose_blueprint`'s `planCard` carries `posture` (objectstack-ai/cloud#2924 ruling A), its `line` (e.g. "This first build goes live at once; later changes are saved as drafts for you to publish") is drawn as the last item of the assumptions list. The exported `ProposedPlanCard` type gains the optional `posture: { mode: 'publish' | 'draft'; line: string }`. A plan card without the key draws as before (objectui#12724).

‎content/docs/plugins/plugin-chatbot.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,8 @@ The AI build conversation's two cards draw only what the build agent's tool
410410
results declare. The plan card (`propose_blueprint`) reads the result's
411411
`planCard`: a chip per field with the format its sample data is held to, a
412412
dashed chip for a field the agent added on its own, the record's name field,
413-
one line per view, the assumptions, and the sample data the build will stage.
413+
one line per view, the assumptions, the sample data the build will stage, and
414+
how the build will land (`posture`: live at once, or saved as a draft).
414415
The result card (`apply_blueprint`, then a later `verify_build` in the same
415416
turn) reads `checklist`: a passed/total chip, the verified lines in two
416417
columns, and the app and package that "Open app" and "View in Studio" open.

‎packages/plugin-chatbot/src/ChatbotEnhanced.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3506,11 +3506,14 @@ const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
35063506
const { designNotes, deferred } = classifyAssumptions(
35073507
tool.proposedPlan!.assumptions,
35083508
);
3509+
const posture = tool.proposedPlan!.planCard?.posture;
35093510
return (
35103511
<>
3511-
{designNotes.length > 0 ? (
3512+
{designNotes.length > 0 || posture ? (
35123513
// objectui#12724 — a list, one item per assumption
3513-
// (`planCard.assumptions`, else `blueprint.assumptions`).
3514+
// (`planCard.assumptions`, else `blueprint.assumptions`),
3515+
// then how the build will land (`planCard.posture.line`,
3516+
// cloud#2924) as the last item, where the artboard draws it.
35143517
<div className="flex flex-col gap-0.5" data-testid="proposed-plan-assumptions">
35153518
<span className="text-[11px] font-medium text-foreground/70">
35163519
{planAssumptionsLabel}
@@ -3519,6 +3522,11 @@ const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
35193522
{designNotes.map((a, idx) => (
35203523
<li key={idx}>{a}</li>
35213524
))}
3525+
{posture ? (
3526+
<li data-testid="proposed-plan-posture" data-posture={posture.mode}>
3527+
{posture.line}
3528+
</li>
3529+
) : null}
35223530
</ul>
35233531
</div>
35243532
) : null}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* objectui#12724 — the plan card says how the build will land (the AiBuild
6+
* artboard's last 假设 item, 「首次搭建直接上线;之后的修改先存草稿,由你发布」) from
7+
* the `planCard.posture` key objectstack-ai/cloud declares (cloud#2924 ruling A;
8+
* `build-cards.schema.json`, vendored as `fixtures/build-cards.schema.ts`). A plan
9+
* card without the key says nothing about it.
10+
*
11+
* Each message goes through the real mapper (`uiMessageToChatMessage`) from a
12+
* raw tool part, so the lift is under test, not a hand-built invocation.
13+
*/
14+
import '@testing-library/jest-dom/vitest';
15+
import { describe, expect, it, vi } from 'vitest';
16+
import { render, screen, within } from '@testing-library/react';
17+
import { I18nProvider } from '@object-ui/i18n';
18+
import { ChatbotEnhanced } from '../ChatbotEnhanced';
19+
import { uiMessageToChatMessage } from '../mapMessages';
20+
import { planEnvelope } from './fixtures/buildCards.fixtures';
21+
22+
const turn = (output: unknown) =>
23+
uiMessageToChatMessage({
24+
id: 'plan',
25+
role: 'assistant',
26+
parts: [{ type: 'tool-propose_blueprint', toolCallId: 'plan-0', state: 'output-available', input: {}, output }],
27+
});
28+
29+
const renderPlan = (envelope: unknown, lang: 'zh' | 'en' = 'zh') =>
30+
render(
31+
<I18nProvider config={{ defaultLanguage: lang, detectBrowserLanguage: false }}>
32+
<ChatbotEnhanced messages={[turn(envelope)]} onSendMessage={vi.fn()} />
33+
</I18nProvider>,
34+
);
35+
36+
const assumptionItems = () =>
37+
within(screen.getByTestId('proposed-plan-assumptions'))
38+
.getAllByRole('listitem')
39+
.map((li) => li.textContent);
40+
41+
describe('objectui#12724 — the plan card draws `planCard.posture.line`', () => {
42+
it('zh: a first build`s line is the last assumption item', () => {
43+
renderPlan(planEnvelope('zh', { posture: 'firstBuild' }));
44+
expect(assumptionItems()).toEqual([
45+
'只建这三部分,不建仪表盘、预约、罚款;补充的字段(分类)可以去掉。',
46+
'归还后状态置为已还;逾期由应还日期已过且未归还判定。',
47+
'首次搭建直接上线;之后的修改先存草稿,由你发布',
48+
]);
49+
expect(screen.getByTestId('proposed-plan-posture')).toHaveAttribute('data-posture', 'publish');
50+
});
51+
52+
it('en: the same line in English', () => {
53+
renderPlan(planEnvelope('en', { posture: 'firstBuild' }), 'en');
54+
expect(screen.getByTestId('proposed-plan-posture')).toHaveTextContent(
55+
/^This first build goes live at once; later changes are saved as drafts for you to publish$/,
56+
);
57+
});
58+
59+
it('a build into a live app draws the producer`s draft line, records included', () => {
60+
renderPlan(planEnvelope('zh', { posture: 'draft' }));
61+
const line = screen.getByTestId('proposed-plan-posture');
62+
expect(line).toHaveTextContent(/^这个应用已上线,有 36 条记录在用;这次的改动先存为草稿,由你在「变更」里核对后发布$/);
63+
expect(line).toHaveAttribute('data-posture', 'draft');
64+
});
65+
66+
it('a plan card with no assumptions still draws the section for the posture line alone', () => {
67+
const plan = planEnvelope('zh', { posture: 'firstBuild' });
68+
renderPlan({ ...plan, planCard: { ...plan.planCard, assumptions: [] } });
69+
expect(assumptionItems()).toEqual(['首次搭建直接上线;之后的修改先存草稿,由你发布']);
70+
});
71+
72+
it('CONTROL: a plan card without `posture` draws only the assumptions', () => {
73+
renderPlan(planEnvelope('zh'));
74+
expect(screen.queryByTestId('proposed-plan-posture')).not.toBeInTheDocument();
75+
expect(assumptionItems()).toHaveLength(2);
76+
});
77+
78+
it('CONTROL: a posture whose mode the schema does not declare is not drawn', () => {
79+
const plan = planEnvelope('zh', { posture: 'firstBuild' });
80+
renderPlan({ ...plan, planCard: { ...plan.planCard, posture: { ...plan.planCard.posture, mode: 'live' } } });
81+
expect(screen.queryByTestId('proposed-plan-posture')).not.toBeInTheDocument();
82+
});
83+
});

‎packages/plugin-chatbot/src/__tests__/buildCards.contract-12724.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ describe('the build-card fixtures are payloads the producer declares (objectui#1
115115
expect(violations(PLAN, planEnvelope(lang, { generatedTitle: true }))).toEqual([]);
116116
});
117117

118+
it.each(['zh', 'en'] as const)('%s: the envelopes with a publish posture satisfy ProposeBlueprintCardPayload (cloud#2924)', (lang) => {
119+
expect(violations(PLAN, planEnvelope(lang, { posture: 'firstBuild' }))).toEqual([]);
120+
expect(violations(PLAN, planEnvelope(lang, { posture: 'draft' }))).toEqual([]);
121+
});
122+
118123
it.each(['zh', 'en'] as const)('%s: the apply_blueprint envelopes satisfy BuildResultCardPayload', (lang) => {
119124
expect(violations(RESULT, applyEnvelope(lang))).toEqual([]);
120125
expect(violations(RESULT, applyEnvelope(lang, { materialized: true }))).toEqual([]);

‎packages/plugin-chatbot/src/__tests__/fixtures/build-cards.schema.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Vendored from objectstack-ai/cloud packages/service-ai-studio/schema/build-cards.schema.json at f7ab9cc8 (cloud PR #3047); the object below is that file verbatim.
1+
// Vendored from objectstack-ai/cloud packages/service-ai-studio/schema/build-cards.schema.json at f0ecef3a (cloud PR #3111); the object below is that file verbatim.
22
//
33
// objectui#12724 — the producer's contract for the two build cards the chat draws:
44
// `ProposeBlueprintCardPayload` (`propose_blueprint`'s `planCard`, beside the
@@ -482,6 +482,33 @@ export const BUILD_CARDS_SCHEMA: Record<string, unknown> =
482482
"line"
483483
],
484484
"additionalProperties": false
485+
},
486+
"posture": {
487+
"type": "object",
488+
"properties": {
489+
"mode": {
490+
"type": "string",
491+
"enum": [
492+
"publish",
493+
"draft"
494+
]
495+
},
496+
"liveRecords": {
497+
"type": "integer",
498+
"minimum": 0,
499+
"maximum": 9007199254740991
500+
},
501+
"line": {
502+
"type": "string",
503+
"minLength": 1
504+
}
505+
},
506+
"required": [
507+
"mode",
508+
"liveRecords",
509+
"line"
510+
],
511+
"additionalProperties": false
485512
}
486513
},
487514
"required": [

‎packages/plugin-chatbot/src/__tests__/fixtures/buildCards.fixtures.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ const W = {
5959
loanTitleTemplate: '{图书} · {读者} · {借出日期}',
6060
loanTitleLine: '名称:自动生成「{图书} · {读者} · {借出日期}」,表单里不出现',
6161
nextSteps: ['给借阅记录加续借次数', '做一个本月借出统计的仪表盘', '只允许管理员删除图书'],
62+
postureFirstBuild: '首次搭建直接上线;之后的修改先存草稿,由你发布',
63+
postureIntoLiveApp: '这个应用已上线,有 36 条记录在用;这次的改动先存为草稿,由你在「变更」里核对后发布',
6264
},
6365
en: {
6466
app: 'Library Lending',
@@ -104,6 +106,9 @@ const W = {
104106
loanTitleTemplate: '{Book} · {Reader} · {Loan date}',
105107
loanTitleLine: 'Name: generated as “{Book} · {Reader} · {Loan date}”, not on the form',
106108
nextSteps: ['Add a renewal count to loans', 'Make a dashboard of this month’s loans', 'Only admins may delete books'],
109+
postureFirstBuild: 'This first build goes live at once; later changes are saved as drafts for you to publish',
110+
postureIntoLiveApp:
111+
'This app is live with 36 records in use; this change is saved as a draft for you to review in Changes and publish',
107112
},
108113
} as const;
109114

@@ -113,9 +118,21 @@ const W = {
113118
* from its two lookups and its date, as cloud#2925's builder writes it: a formula
114119
* `nameField` on the blueprint, and on the plan card `nameField.generated` with
115120
* the title field left out of the loan's chips (its line stands for it).
121+
* `posture` (cloud#2924 ruling A) adds `planCard.posture` as `planCardOf`
122+
* writes it: `'firstBuild'` for a new app's first build, `'draft'` for a build
123+
* into an app live with 36 records.
116124
*/
117-
export function planEnvelope(lang: Lang, opts: { generatedTitle?: boolean } = {}) {
125+
export function planEnvelope(
126+
lang: Lang,
127+
opts: { generatedTitle?: boolean; posture?: 'firstBuild' | 'draft' } = {},
128+
) {
118129
const w = W[lang];
130+
const posture =
131+
opts.posture === 'firstBuild'
132+
? { mode: 'publish' as const, liveRecords: 0, line: w.postureFirstBuild }
133+
: opts.posture === 'draft'
134+
? { mode: 'draft' as const, liveRecords: 36, line: w.postureIntoLiveApp }
135+
: undefined;
119136
const loanTitleField = {
120137
name: 'title',
121138
label: w.loanTitle,
@@ -280,6 +297,7 @@ export function planEnvelope(lang: Lang, opts: { generatedTitle?: boolean } = {}
280297
],
281298
line: w.sampleLine,
282299
},
300+
...(posture ? { posture } : {}),
283301
},
284302
};
285303
}

‎packages/plugin-chatbot/src/mapMessages.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,15 @@ export interface ProposedPlanCard {
372372
assumptions: string[];
373373
/** The sample data the build stages, as one line. Absent when it stages none. */
374374
sampleData?: { line: string };
375+
/**
376+
* cloud#2924 ruling A — how the build will land once approved: `publish` (a
377+
* new app's first build, or an app with no records yet) or `draft` (an app
378+
* live with records), and the card's `line` (e.g. 「首次搭建直接上线;之后的修改
379+
* 先存草稿,由你发布」). Absent where the build does not publish in the turn,
380+
* and the card then says nothing about it. The producer's `liveRecords` is
381+
* already in the line, so it is not lifted.
382+
*/
383+
posture?: { mode: 'publish' | 'draft'; line: string };
375384
}
376385

377386
/**
@@ -552,11 +561,15 @@ function liftPlanCard(raw: unknown): ProposedPlanCard | undefined {
552561
];
553562
});
554563
const sampleData = recordOf(card.sampleData);
564+
const posture = recordOf(card.posture);
555565
return {
556566
objects,
557567
views: viewLines(card.views),
558568
assumptions: nonEmptyStrings(card.assumptions),
559569
...(sampleData && isText(sampleData.line) ? { sampleData: { line: sampleData.line } } : {}),
570+
...(posture && (posture.mode === 'publish' || posture.mode === 'draft') && isText(posture.line)
571+
? { posture: { mode: posture.mode, line: posture.line } }
572+
: {}),
560573
};
561574
}
562575

0 commit comments

Comments
 (0)