Skip to content

Commit a552e86

Browse files
fix(plugin-chatbot): a plan that overlaps an existing app asks whether to extend it or build a new one, instead of offering Build it (#27595)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4i9JiKjEjwtuVzd4gKy9hB)_ Fixes #27480 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:AI 搭建方案若与用户已有的应用重叠(结果带 `decision: 'iterate_or_new'`),方案卡不再显示「开始搭建」,改为提问「在「X」上继续完善,还是另建一个独立应用」,两个选项各自发出对应回复;普通方案不变。 - 为什么改:之前这类结果被画成普通方案卡,一键「开始搭建」就会造出一个和已有应用重复的新应用。 - 风险与回滚:只在识别到 `iterate_or_new` 时换掉按钮行,其余方案走原路径;回复文字按会话语言发出,且不会被确认门当作同意;回滚即还原本 PR。 - 先看:`ui/packages/plugin-chatbot/src/ChatbotEnhanced.tsx` 的 `IterateOrNewQuestion` 与按钮行三分支;其次 `mapMessages.ts` 的 `liftIterateOrNew`;再看 `useChatConversation.ts` 缓存回写。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.iterateOrNew-27480.test.tsx`,预期 10 个测试全绿。 - 需要你做的:无,席位落地。 ## What changes - **Detection (`mapMessages.ts`).** `detectProposedPlan` lifts `decision: 'iterate_or_new'` into a new optional `ProposedPlan.iterateOrNew`, with the overlapping app's `name` and optional `label` read from `existingAppOverlap`. A result with the decision but no objects still yields a plan, so the card asks the question instead of falling back to the generic "Build it" card. An overlap that names no app yields `iterateOrNew: {}`. - **The question (`ChatbotEnhanced.tsx`).** For such a plan the card keeps its rows and replaces "Build it" / "Adjust" with `IterateOrNewQuestion`. The question names the app (`label`, else the host's `resolveAppLabel(name)`, else `name`). Its two choices are "Keep improving X" and "Build a separate, new app", and each sends its answer through `onSendMessage`. Without a send channel, or without an app name, the card shows the question and a reply hint and offers no one-click choice. Once a build has run after the plan, the question stays and the choices go, as the plan's own buttons already do. - **Outbound answers.** Two new optional props, `planExtendExistingMessage(app)` and `planBuildNewAppMessage`, with English defaults. The console wires both through `resolveOutboundAgentText`, so they follow the conversation's language like the plan card's other outbound messages. The new keys `console.ai.planExtendExistingMessage` and `console.ai.planBuildNewAppMessage` are in the `en` and `zh` packs only, as the other outbound keys are. - **Reload.** Server history re-parses the stored result with the same detector. The message cache's compact plan envelope (`proposedPlanToCachedResult`) now writes back `decision` and `existingAppOverlap`, so a cache-fallback reload asks the question too. - **Labels.** `chatbot.plan.iterateOrNew.{question, questionUnnamed, extend, buildNew, replyHint}` in all ten packs. - **Docs.** One paragraph in `content/docs/frontend/plugins/plugin-chatbot.mdx` after the plan-card description. - One patch changeset for `@object-ui/plugin-chatbot`, `@object-ui/app-shell` and `@object-ui/i18n`. ## Tests - `mapMessages.iterateOrNew-27480.test.ts` (7): the lift from the bare result and from the `{type, value}` wrapper; a plain result has no `iterateOrNew`; another `decision` value is ignored; a result with no objects still yields a plan; `label` is optional and an overlap with no app gives `{}`; the live mapper carries the field. - `ChatbotEnhanced.iterateOrNew-27480.test.tsx` (10): no approve, adjust or fallback button and the question names the app (asserted first); the plain proposal keeps "Build it" and "Adjust" (control); each choice sends its answer; the host's outbound text is sent when supplied; zh labels; no objects still asks; `resolveAppLabel`; the unnamed overlap; no `onSendMessage`; the settled state after a build. - `AiChatPage.iterateOrNewReload-27480.test.tsx` (3): server history re-parses to the question; the cache fallback keeps both keys and both re-maps render the question; a plain proposal is cached without the keys (control). - `AiChatPage.planCardLocale.test.tsx`: the two answers follow the conversation, not the UI locale (zh thread under en/zh/de UI, en thread under zh/en/de UI). - `outboundAgentText.test.ts`, `outbound-agent-messages.test.ts`, `all-locales-key-parity.test.ts`, `console-namespace-3546.test.tsx`: the two outbound keys are registered. `i18n.test.ts`: neither answer, in zh or en, matches the confirm gate's approval pattern. ## Acceptance notes - **The result shape is read flat and unverified against the producer.** The cloud half (cloud#3369) was not readable from this container, so the reader takes `existingAppOverlap.name` / `.label` as the card quotes them and tolerates their absence: a missing or differently nested app gives the unnamed question with a reply hint, never the "Build it" card. The cloud#3369 owner should confirm the nesting. - **Ablation** through `scripts/ablation-replace.mjs`, each anchor x1 to x0 with the blob changed, and each file restored to equal HEAD with `git diff HEAD` empty: - A1, the lift always returns nothing (`mapMessages.ts`): 16 red, 4 green (the plain controls) over the three new files. - A2, the question face off (`ChatbotEnhanced.tsx`): 11 red, 9 green, with the mapper tests green. A2b, run alone on the first test, fails on the missing-approve assertion. - A3, the cache write-back off (`useChatConversation.ts`): only the cache-fallback test red. - A4, the console's outbound wiring bypassed (`AiChatPage.tsx`): the six conversation-language tests red. - Three i18n test files outside the named surface (`all-locales-key-parity`, `console-namespace-3546`, `i18n.test.ts`) gained the two keys. They are the same key ledgers the named `outbound-agent-messages.test.ts` reads. - The plugin README lists no plan-card props, so it is unchanged. The docs paragraph is the documented surface. ## Verification See the PR's check-runs for every family CI runs. Local readings (targeted vitest, type-check of the three packages, the derived gate list) are in the os-dev report on #27480. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6Ca9ZbSdPg2iuzBYA3d)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6Ca9ZbSdPg2iuzBYA3d --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91e9797 commit a552e86

27 files changed

Lines changed: 774 additions & 12 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/plugin-chatbot': patch
3+
'@object-ui/app-shell': patch
4+
'@object-ui/i18n': patch
5+
---
6+
7+
fix(plugin-chatbot): a plan that overlaps an existing app asks whether to extend it or build a new one, instead of offering Build it
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
When `propose_blueprint` finds that the plan overlaps an app the user already has, its result carries `decision: 'iterate_or_new'` and the overlapping app in `existingAppOverlap`. The chat used to draw that result as an ordinary plan card with a "Build it" button, so one click could build a duplicate of the existing app.
13+
14+
- **The question.** `detectProposedPlan` lifts the decision as `ProposedPlan.iterateOrNew`, with the app's `name` and `label` from `existingAppOverlap`. The plan card keeps its rows and, in place of "Build it" / "Adjust", asks whether to keep improving that app or build a separate one. Each of the two choices sends its answer as a chat message. A result that names no object still gets the question, never the fallback "Build it" card; an overlap that names no app gets the question without a name and a reply hint.
15+
- **The answers follow the conversation.** `ChatbotEnhanced` takes two new optional props, `planExtendExistingMessage(app)` and `planBuildNewAppMessage`. The console resolves both by the conversation's language, like the plan card's other outbound messages (`console.ai.planExtendExistingMessage` and `console.ai.planBuildNewAppMessage`, in the `en` and `zh` packs only). Neither reads as approval to the confirm gate.
16+
- **After a reload.** Server history re-parses the stored result. The message cache now writes `decision` and `existingAppOverlap` back into its compact plan envelope, so a cache-fallback reload asks the question too.
17+
- **Labels.** `chatbot.plan.iterateOrNew.*` (question, choices, reply hint) in all ten packs.

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,13 @@ results declare. The plan card (`propose_blueprint`) reads the result's
496496
dashed chip for a field the agent added on its own, the record's name field,
497497
one line per view, the assumptions, the sample data the build will stage, and
498498
how the build will land (`posture`: live at once, or saved as a draft).
499+
A plan whose result says `decision: 'iterate_or_new'` overlaps an app the user
500+
already has (`existingAppOverlap`, by `name` and `label`). Its card draws no
501+
"Build it" button: in its place it asks whether to keep improving that app or
502+
build a separate one, naming the app, and each choice sends its answer as a
503+
chat message (`planExtendExistingMessage(app)` and `planBuildNewAppMessage`,
504+
which the console picks by the conversation's language). The question survives
505+
a reload from server history and from the console's message cache.
499506
The result card (`apply_blueprint`, then a later `verify_build` in the same
500507
turn) reads `checklist`: a passed/total chip, the verified lines in two
501508
columns, and the app and package that "Open app" and "View in Studio" open.

‎ui/packages/app-shell/src/console/ai/AiChatPage.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2875,6 +2875,11 @@ export function ChatPane({
28752875
planAnswerMessage={(question, option) =>
28762876
outboundText('planAnswerMessage', { question, option })
28772877
}
2878+
// objectstack#27480 — the two answers of the extend-or-new question an
2879+
// `iterate_or_new` plan asks: outbound text, so the conversation's
2880+
// language picks them, like every message above.
2881+
planExtendExistingMessage={(app) => outboundText('planExtendExistingMessage', { app })}
2882+
planBuildNewAppMessage={outboundText('planBuildNewAppMessage')}
28782883
// Self-use "magic moment": when the plan enables it, publish the drafted
28792884
// app automatically the moment the agent finishes — no manual click; the
28802885
// user refreshes and sees it live WITH data. Same governed endpoint.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#27480 — the extend-or-new question an `iterate_or_new` plan asks
5+
* survives a reload, on both reload paths this page has. Without that, a reload
6+
* turns the question back into an ordinary plan card with a "Build it" button:
7+
* the one-click duplicate the question exists to prevent, one refresh later.
8+
*
9+
* - server history: the stored tool result is merged back onto its call
10+
* (`toUIMessages`) and re-parsed by the same detector the live chat uses;
11+
* - cache fallback: the cache keeps a compact `output` rebuilt from the
12+
* derived plan (`sanitizeChatMessagesForCache`), so the question's keys
13+
* must be written back into it.
14+
*
15+
* Each path ends where the user does: the card rendered from the reloaded
16+
* messages, asserted for the question and for the absence of "Build it".
17+
*/
18+
import '@testing-library/jest-dom/vitest';
19+
import { afterEach, describe, expect, it, vi } from 'vitest';
20+
import { cleanup, render, screen } from '@testing-library/react';
21+
import React from 'react';
22+
import { ChatbotEnhanced, uiMessageToChatMessage } from '@object-ui/plugin-chatbot';
23+
import { hydratedMessagesToChatMessages } from '../AiChatPage';
24+
import { sanitizeChatMessagesForCache, toUIMessages } from '../../../hooks/useChatConversation';
25+
26+
const ITERATE_OR_NEW = {
27+
status: 'blueprint_proposed',
28+
decision: 'iterate_or_new',
29+
existingAppOverlap: { name: 'library', label: '图书馆' },
30+
summary: 'A reading-list app',
31+
blueprint: {
32+
objects: [{ name: 'book', label: 'Book', fields: [{ name: 'title' }, { name: 'author' }] }],
33+
},
34+
counts: { objects: 1, views: 0, dashboards: 0, seedData: 0 },
35+
questions: [],
36+
};
37+
38+
const LIVE = {
39+
id: 'a1',
40+
role: 'assistant' as const,
41+
parts: [
42+
{
43+
type: 'tool-propose_blueprint',
44+
toolCallId: 'plan-1',
45+
state: 'output-available' as const,
46+
input: {},
47+
output: ITERATE_OR_NEW,
48+
},
49+
],
50+
};
51+
52+
function renderCard(messages: React.ComponentProps<typeof ChatbotEnhanced>['messages']) {
53+
render(<ChatbotEnhanced messages={messages} onSendMessage={vi.fn()} />);
54+
}
55+
56+
function expectQuestionNotApproval() {
57+
expect(screen.getByTestId('proposed-plan-iterate-or-new-question').textContent).toContain('图书馆');
58+
expect(screen.getByTestId('proposed-plan-extend-existing')).toBeInTheDocument();
59+
expect(screen.getByTestId('proposed-plan-build-new')).toBeInTheDocument();
60+
expect(screen.queryByTestId('proposed-plan-approve')).toBeNull();
61+
expect(screen.queryByTestId('proposed-plan-fallback')).toBeNull();
62+
}
63+
64+
afterEach(cleanup);
65+
66+
describe('the iterate_or_new question survives a reload (objectstack#27480)', () => {
67+
it('server history: the stored result re-parses to the question', () => {
68+
const ui = toUIMessages([
69+
{
70+
id: 'a1',
71+
role: 'assistant',
72+
content: [{ type: 'tool-call', toolCallId: 'plan-1', toolName: 'propose_blueprint', input: {} }],
73+
},
74+
{
75+
role: 'tool',
76+
content: [
77+
{
78+
type: 'tool-result',
79+
toolCallId: 'plan-1',
80+
output: { type: 'text', value: JSON.stringify(ITERATE_OR_NEW) },
81+
},
82+
],
83+
},
84+
]);
85+
const messages = hydratedMessagesToChatMessages(ui);
86+
expect(messages[0]?.toolInvocations?.[0]?.proposedPlan?.iterateOrNew).toEqual({
87+
app: { name: 'library', label: '图书馆' },
88+
});
89+
renderCard(messages);
90+
expectQuestionNotApproval();
91+
});
92+
93+
it('cache fallback: the compact cached output keeps both keys the question is read from', () => {
94+
const live = uiMessageToChatMessage(LIVE);
95+
const persisted = JSON.parse(JSON.stringify(sanitizeChatMessagesForCache([live]))) as ReturnType<
96+
typeof sanitizeChatMessagesForCache
97+
>;
98+
const planPart = persisted[0]?.parts.find((p) => p.type === 'tool-propose_blueprint');
99+
expect(planPart?.output).toMatchObject({
100+
status: 'blueprint_proposed',
101+
decision: 'iterate_or_new',
102+
existingAppOverlap: { name: 'library', label: '图书馆' },
103+
});
104+
105+
// Both reads a cache-fallback reload performs: this page's hydration
106+
// mapper, and the live mapper `useObjectChat` re-runs over the same parts.
107+
for (const messages of [hydratedMessagesToChatMessages(persisted), [uiMessageToChatMessage(persisted[0])]]) {
108+
expect(messages[0]?.toolInvocations?.[0]?.proposedPlan?.iterateOrNew).toEqual({
109+
app: { name: 'library', label: '图书馆' },
110+
});
111+
cleanup();
112+
renderCard(messages);
113+
expectQuestionNotApproval();
114+
}
115+
});
116+
117+
it('cache fallback: a plain proposal is cached without the question keys (control)', () => {
118+
const plain = { ...ITERATE_OR_NEW, decision: undefined, existingAppOverlap: undefined };
119+
const live = uiMessageToChatMessage({ ...LIVE, parts: [{ ...LIVE.parts[0], output: plain }] });
120+
const persisted = sanitizeChatMessagesForCache([live]);
121+
const output = persisted[0]?.parts.find((p) => p.type === 'tool-propose_blueprint')?.output as Record<
122+
string,
123+
unknown
124+
>;
125+
expect(output.status).toBe('blueprint_proposed');
126+
expect('decision' in output).toBe(false);
127+
expect('existingAppOverlap' in output).toBe(false);
128+
renderCard(hydratedMessagesToChatMessages(persisted));
129+
expect(screen.getByTestId('proposed-plan-approve')).toBeInTheDocument();
130+
});
131+
});

‎ui/packages/app-shell/src/console/ai/__tests__/AiChatPage.planCardLocale.test.tsx‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,35 @@ describe('#3896 — outbound text follows the CONVERSATION, in both directions',
276276
);
277277
});
278278
});
279+
280+
/**
281+
* objectstack#27480 — the extend-or-new question an `iterate_or_new` plan asks
282+
* SENDS one of two answers, so they sit on the send side of the same line: the
283+
* conversation's language picks them, never the console's. The rows that can
284+
* discriminate are the ones where the two disagree.
285+
*/
286+
describe('objectstack#27480 — the extend-or-new answers follow the CONVERSATION', () => {
287+
function answers(props: Record<string, unknown>) {
288+
return {
289+
extend: (props.planExtendExistingMessage as (app: string) => string)('图书馆'),
290+
buildNew: props.planBuildNewAppMessage,
291+
};
292+
}
293+
294+
it.each(['en', 'zh', 'de'])('%s UI + Chinese thread: both answers are Chinese', (uiLocale) => {
295+
expect(answers(renderPane(uiLocale, ZH_THREAD))).toEqual({
296+
extend: '在「图书馆」上继续完善,不另建应用。',
297+
buildNew: '另建一个独立应用。',
298+
});
299+
});
300+
301+
it.each(['zh', 'en', 'de'])('%s UI + English thread: both answers are English', (uiLocale) => {
302+
const props = renderPane(uiLocale, EN_THREAD);
303+
expect(answers(props)).toEqual({
304+
extend: 'Keep improving "图书馆" instead of building a new app.',
305+
buildNew: 'Build a separate, new app.',
306+
});
307+
// Nothing Chinese leaves the console for an English thread but the app's own name.
308+
expect(CJK.test(String(props.planBuildNewAppMessage)), `outbound Chinese under ${uiLocale} UI`).toBe(false);
309+
});
310+
});

‎ui/packages/app-shell/src/console/ai/__tests__/outboundAgentText.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* and nothing else. There is deliberately no `I18nProvider` in this file — a UI
99
* locale cannot even be expressed here, which is the strongest statement of the
1010
* rule the unit level can make. `AiChatPage.planCardLocale.test.tsx` then pins
11-
* the same four strings through a real provider, where a UI locale exists and
11+
* the same strings through a real provider, where a UI locale exists and
1212
* could still leak in.
1313
*
1414
* Gate conformance is NOT re-asserted here: the cloud `APPROVAL_RE` mirror lives
@@ -29,6 +29,8 @@ const KEYS = [
2929
'planApproveMessage',
3030
'planApproveDefaultsMessage',
3131
'planAnswerMessage',
32+
'planExtendExistingMessage',
33+
'planBuildNewAppMessage',
3234
'changesConfirmMessage',
3335
] as const;
3436

@@ -95,4 +97,18 @@ describe('outbound agent text follows the conversation, never the UI locale (#38
9597
expect(resolveOutboundAgentText(false, 'planAnswerMessage')).toBe(packValue(en, 'planAnswerMessage'));
9698
});
9799
});
100+
101+
describe('planExtendExistingMessage names the app (objectstack#27480)', () => {
102+
it('fills the app in a Chinese conversation', () => {
103+
expect(resolveOutboundAgentText(true, 'planExtendExistingMessage', { app: '图书馆' })).toBe(
104+
'在「图书馆」上继续完善,不另建应用。',
105+
);
106+
});
107+
108+
it('fills the app in an English conversation', () => {
109+
expect(resolveOutboundAgentText(false, 'planExtendExistingMessage', { app: 'Library' })).toBe(
110+
'Keep improving "Library" instead of building a new app.',
111+
);
112+
});
113+
});
98114
});

‎ui/packages/app-shell/src/console/ai/outboundAgentText.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
// worse: never gated at all, so it sent the UI locale's value in both
2020
// directions.
2121
//
22-
// Hence this module: ONE resolver, four sites, both directions, and the UI pack
22+
// Hence this module: ONE resolver, every outbound site, both directions, and the UI pack
2323
// is never consulted. Chinese conversation -> the `zh` pack's value; anything
2424
// else -> the `en` pack's value. Reading the built-in packs directly (instead of
2525
// `t(key, { lng })`) is deliberate: an i18next lookup's answer depends on which
@@ -38,6 +38,8 @@ export type OutboundAgentTextKey =
3838
| 'planApproveMessage'
3939
| 'planApproveDefaultsMessage'
4040
| 'planAnswerMessage'
41+
| 'planExtendExistingMessage'
42+
| 'planBuildNewAppMessage'
4143
| 'changesConfirmMessage';
4244

4345
/**
@@ -63,6 +65,17 @@ const GATE_TEXT: Record<OutboundAgentTextKey, Record<'zh' | 'en', string>> = {
6365
zh: '关于「{{question}}」,我选择「{{option}}」。',
6466
en: 'For "{{question}}", go with: {{option}}.',
6567
},
68+
// objectstack#27480 — the two answers of the extend-or-new question an
69+
// `iterate_or_new` plan asks. Like `planAnswerMessage` they answer a question
70+
// rather than approve a plan.
71+
planExtendExistingMessage: {
72+
zh: '在「{{app}}」上继续完善,不另建应用。',
73+
en: 'Keep improving "{{app}}" instead of building a new app.',
74+
},
75+
planBuildNewAppMessage: {
76+
zh: '另建一个独立应用。',
77+
en: 'Build a separate, new app.',
78+
},
6679
changesConfirmMessage: {
6780
zh: '确认修改,应用你刚才提议的改动。',
6881
en: 'Confirm — apply the change you just proposed.',
@@ -106,9 +119,9 @@ export function resolveOutboundAgentText(
106119
// the boots where the console is running in Chinese. When it is not — an
107120
// English console answering a Chinese thread — this falls through to
108121
// {@link GATE_TEXT}, which `outboundAgentText.test.ts` pins BYTE-IDENTICAL to
109-
// the `zh` pack's four values, so the text that leaves is the same either
122+
// the `zh` pack's outbound values, so the text that leaves is the same either
110123
// way. ⛔ Do not import the `zh` pack back to close that branch: it would put
111-
// a second 156 KB catalogue into every page load to re-derive four strings
124+
// a second 156 KB catalogue into every page load to re-derive strings
112125
// that are already pinned equal.
113126
const template =
114127
packText(getLoadedBuiltInLocales()[lang], key) ?? GATE_TEXT[key][lang];

‎ui/packages/app-shell/src/hooks/useChatConversation.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ interface CachedProposedPlan {
167167
questions: string[];
168168
assumptions: string[];
169169
targetApp?: string;
170+
/**
171+
* objectstack#27480 — the extend-or-new question a `decision: 'iterate_or_new'`
172+
* plan asks. Kept through the cache because without it a cache-fallback
173+
* reload turns the question back into a plan with a "Build it" button.
174+
*/
175+
iterateOrNew?: { app?: { name: string; label?: string } };
170176
}
171177

172178
interface CacheableChatToolInvocation {
@@ -256,6 +262,13 @@ function proposedPlanToCachedResult(pp: CachedProposedPlan): Record<string, unkn
256262
counts: pp.counts,
257263
questions: pp.questions,
258264
...(pp.targetApp ? { targetApp: pp.targetApp } : {}),
265+
// objectstack#27480 — the two keys `detectProposedPlan` reads the question from.
266+
...(pp.iterateOrNew
267+
? {
268+
decision: 'iterate_or_new',
269+
...(pp.iterateOrNew.app ? { existingAppOverlap: pp.iterateOrNew.app } : {}),
270+
}
271+
: {}),
259272
blueprint: {
260273
objects: pp.objects.map((o) => ({
261274
name: o.name,

‎ui/packages/i18n/src/__tests__/all-locales-key-parity.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ const OUTBOUND_KEYS = new Set([
116116
'console.ai.planApproveMessage',
117117
'console.ai.planApproveDefaultsMessage',
118118
'console.ai.planAnswerMessage',
119+
'console.ai.planExtendExistingMessage',
120+
'console.ai.planBuildNewAppMessage',
119121
'console.ai.changesConfirmMessage',
120122
]);
121123

‎ui/packages/i18n/src/__tests__/console-namespace-3546.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@ describe('objectui#3546 slice four — the console namespace', () => {
376376
'planAnswerMessage',
377377
'planApproveDefaultsMessage',
378378
'planApproveMessage',
379+
'planBuildNewAppMessage',
380+
'planExtendExistingMessage',
379381
]);
380382
// #3896's headline defect in one line: an outbound message read through
381383
// `t()` is read from the UI pack, which is how a zh console sent Chinese

0 commit comments

Comments
 (0)