Skip to content

Commit fccde75

Browse files
fix(plugin-chatbot,app-shell): the follow-up sent to the model after Confirm or Cancel is no longer shown as the person's message (#30296)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB3raJW9Z4ixrGxfuEz9hefp)_ Fixes #30223 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:聊天确认卡片点「确认」或「取消」后,系统发给模型的那段续写指令(`[HITL pa_…] The operator approved …`)不再显示成用户自己的气泡,对话列表预览也不再显示它;模型照常收到这段文字。 - 为什么改:客户在自己那一侧看到一大段带 id 和 JSON 的英文,像泄露或故障;重新打开对话、左侧对话列表里也一直显示它。 - 风险与回滚:只改界面读取,存储和发给模型的内容一字不变;识别靠续写文字开头的 `[HITL 编号]` 标记,用户自己恰好以这种格式开头打字才会被误隐藏。回滚即还原本 PR。 - 先看:`ui/packages/plugin-chatbot/src/hitlContinuation.ts`(标记的唯一出处)、`ChatbotEnhanced.tsx` 中 `isHitlContinuation` 那一行、`useConversationList.ts` 的 `extractPreview`。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/plugin-chatbot/src/__tests__/hitlContinuation-30223.test.tsx`,预期 5 例全绿;截图见下方 Verification。 - 需要你做的:无,席位落地。 ## What changes - **One marker, one module.** New `ui/packages/plugin-chatbot/src/hitlContinuation.ts` owns the continuation tag: `hitlContinuationTag(id)` writes `[HITL PA_ID]`, `isHitlContinuation(text)` reads it (tag at the very start, one non-space id, then a space). `useHitlInChat` now builds its prompt with `hitlContinuationTag`, so producer and reader cannot drift. The text sent to the model is byte-identical to before. - **Transcript.** `ChatbotEnhanced` draws no bubble for a user message whose content is a continuation. That one seam covers the live chat, a reloaded conversation, the dock (it mounts the same `ChatPane`) and the shared-transcript page (it renders `ChatbotEnhanced` read-only). - **Conversation list preview.** `useConversationList`'s `extractPreview` reads past continuations to the person's last real ask; a server-supplied `preview` that is a continuation is dropped too. The list's search haystack is built from the same `preview`, so search stops matching the continuation as well. - **Conversation-language probe.** `detectConversationLanguage` (app-shell) skipped nothing: after a confirm, the latest "user" turn was the English continuation, so a Chinese conversation's outbound text (plan approve, change-confirm send text) turned English whenever the action's result carried no Chinese. It now reads past the continuation. Same marker, same defect class (the continuation read as the person's turn), one line. - **Export.** `isHitlContinuation` is exported from `@object-ui/plugin-chatbot` and `@object-ui/plugin-chatbot/core` (the app-shell readers import it from `./core`, which stays side-effect free). The producer `hitlContinuationTag` stays internal. ## Why The ruling asked to prefer a marker carried on the message (metadata or a part flag) over the `[HITL …]` prefix, and to stop if the clean fix needs a server change. Measured: **nothing but the text survives storage.** - The agent route collapses an inbound UI message's `parts` to their joined text (`normalizeMessage` in `@objectstack/service-ai`), and `addMessage` persists `content` as that string. - `ai_messages` has the columns `id`, `conversation_id`, `role`, `content`, `tool_calls`, `tool_call_id`, `turn_id` and token/latency columns; no column holds message metadata. `GET /conversations/:id` rebuilds each user message as `{ role: 'user', content: row.content }`. - The share endpoint's raw rows (`RawAiMessageRow` in `useChatConversation.ts`) carry the same columns. Read from the last published `@objectstack/service-ai` (10.3.0, the npm tarball); the current service-ai is Cloud/EE and not in this repository, so its source was not read. The staging evidence on the card (the continuation text in the stored history and the list preview) agrees. A UI `metadata` flag would therefore exist only until the first reload, and two markers would be two dialects. The leading tag is the one marker that round-trips, so it is the marker, and its spelling now lives in one module next to its reader. No server, REST or spec change. Seam for a structural marker later, if wanted: `cloud:service-ai normalizeMessage + ai_messages (no metadata column) → renderer:ChatbotEnhanced, app-shell:useConversationList`. ## Tests New pins (all on the real hooks and renderer, no component mocks): - `plugin-chatbot/src/__tests__/hitlContinuation-30223.test.tsx` — `useHitlInChat` decides over a mocked pending-actions endpoint and hands its continuation to the REAL `useObjectChat` transport. 确认 leg and 取消 leg: the captured chat POST's last user message is the full continuation, tag first (the model-facing payload); the hook holds it as a user message; `ChatbotEnhanced` (through `toRuntimeMessages`, as the console page does) draws the person's ask and no continuation. CONTROL: an ordinary typed message is sent verbatim and drawn. Reader unit: the producer's tag reads true; ordinary text, a tag not at the start, `[HITL]` with no id and non-strings read false. - `app-shell/src/console/ai/__tests__/hitlContinuation-30223.test.tsx` — reload leg over stored rows through the real hydration path (`aiMessageRowsToServerMessages` → `toUIMessages` → `hydratedMessagesToChatMessages` → `ChatbotEnhanced`): the continuation hydrates as a user message and draws no bubble; CONTROL: an ordinary stored user message still draws. Language probe: a Chinese ask followed by an English continuation stays `zh-CN`; CONTROL: an English turn the person typed still decides. - `app-shell/src/hooks/__tests__/useConversationList.hitlContinuation-30223.test.tsx` — the confirmed conversation previews the ask, not the continuation stored after it; CONTROL: a conversation without a confirm previews its last user message unchanged; a conversation holding only a continuation (and a served `preview` equal to it) gets no preview rather than the continuation. <details><summary>Ablation (one per reader; mutation landed, restored, blob == HEAD)</summary> Run through `scripts/ablation-replace.mjs` on commit `6244d3a5`; each mutation reported `anchor 1 -> 0` and each restore `blob == HEAD`, `git diff HEAD` empty. The ui suites import `@object-ui/*` from source, so no rebuild between legs. - A1 `ChatbotEnhanced.tsx`: `if (isUser && isHitlContinuation(message.content)) return null;` → `… && false …`. Red: 确认 leg, 取消 leg, reload leg (3 failed / 6 passed; every CONTROL stayed green). - A2 `useConversationList.ts`: `if (text && !isHitlContinuation(text)) return text;` → `if (text) return text;`. Red: `Expected "把这条状态改成 Active"`, `Received "[HITL pa_8cf40802-… The operator approved the update_record call…"`. - A3 `conversationLanguage.ts`: the `continue` on a continuation disabled. Red: "a Chinese conversation stays Chinese after an English continuation" (1 failed / 3 passed). </details> <details><summary>Suites and type-check</summary> On `6244d3a5` under `scripts/pm/os-verify-lock.sh`, from `ui/`: - `vitest run packages/plugin-chatbot/` — Test Files 95 passed (95), Tests 880 passed (880). - `vitest run` over the 140 app-shell test files that import `useConversationList`, `conversationLanguage`, `AiChatPage`, `ConversationsSidebar` or `ChatDock` — Test Files 140 passed (140), Tests 992 passed (992). Narrowed: the rest of app-shell does not import a touched module; CI's Test UI runs it all. - `pnpm --filter @object-ui/app-shell type-check` exit 0. `pnpm --filter @object-ui/plugin-chatbot type-check` first exited 2 on the new test (hook output handed to `ChatbotEnhanced` without `toRuntimeMessages`); fixed in `f572a55d`, then exit 0. After merging `origin/main` (`632f7d95`): the three new files plus `useHitlInChat.test.tsx` — Test Files 4 passed (4), Tests 19 passed (19). </details> ## Acceptance notes - **Card Acceptance.** (1) After 确认 or 取消 no continuation bubble: hidden (the one-line note option was not taken; it would need a new label in all ten locale packs). (2) The model still receives the continuation: pinned on the captured chat POST. (3) A reloaded conversation does not show it, including the conversation list (comment 6648541899): pinned on the hydration path and on `useConversationList`. - **Recognition is by the leading tag.** A person who types a message that itself starts with `[HITL ` + an id + `] ` would not see that bubble. The tag is not something the product ever asks a person to type. - **Out of scope, reported not fixed** (the card's two smaller things): the assistant reply sometimes names the record by its id (that is model wording over the result JSON, which carries `recordId` and a `link` with the label); the record page behind the dock does not refresh after a chat write. Neither is touched here. - **File surface beyond the claim's list.** Besides `useHitlInChat.ts`, the transcript renderer and the list-preview reader, this diff adds `plugin-chatbot/src/hitlContinuation.ts`, two export lines (`core.ts`, `index.tsx`) and the language-probe line in `app-shell/src/console/ai/conversationLanguage.ts` (another reader of the same stored turn, same defect class). ## Verification Live leg NOT run: the in-product agent runtime (`@objectstack/service-ai`) is Cloud/EE and absent from the open framework (`/ai/**` answers 501 there), and no LLM provider is configured in this container. Closest deterministic fixture instead: - The mock-transport legs above (real `useHitlInChat` + real `useObjectChat` transport + real renderer). - A real-Chromium render of the console's own modules (a scratch dev-server page, not committed): `ConversationsSidebar` over a stubbed `GET /conversations`, beside `ChatbotEnhanced`, both fed a transcript with one confirmed update and one cancelled create. Before (both readers swapped back to `origin/main`, served code checked): the cancel continuation shows as a user bubble and as the list preview. After: neither; the list previews the last real ask 「再新建一个客户 验收客户 R6」. Text probe of the page: before `chatHasHitl: true, listHasHitl: true`; after `false, false`. Shots: `/mnt/project-files/ui-orders/30223-shots/before-fix.png`, `after-fix.png` (project files). <details><summary>Derived gates: 35 run, 35 exit 0 (HEAD 632f7d9)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `632f7d95` (after merging `origin/main`, merge base `959c26be1`) derived 35 commands; each was run with its exit code captured before any pipe, all 35 exit 0. `--ran` reconciliation: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED". The roster family the derivation flagged under `.changeset` was run as well: `pnpm check:changeset-fixed` exit 0. Type-check lanes, artifact rosters and wide-population families are CI's (the PR check-runs). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (measured locally as `npx eslint` over the touched files from each package: 0 errors; the new `index.tsx` export line adds one `react-refresh/only-export-components` warning, the barrel's existing pattern. Repo-wide `pnpm lint` is CI's run.) - [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. (not touched) - [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. (the maintainer brief is in Chinese by repo rule) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfQigsiRb7grXXevft --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 977be38 commit fccde75

11 files changed

Lines changed: 480 additions & 4 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/plugin-chatbot': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL <id>]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#30223 — the follow-up turn the chat sends the model after
5+
* 确认 / 取消 is stored as an ordinary user message (the store keeps only the
6+
* text). A reloaded conversation must not draw it as the person's bubble, and
7+
* the conversation-language probe must not read it as the person's latest turn.
8+
*
9+
* The reload leg drives the real hydration path over stored rows, exactly as
10+
* `recordWriteRehydration-3565` does, then draws the result with the real
11+
* `ChatbotEnhanced`.
12+
*/
13+
14+
import '@testing-library/jest-dom/vitest';
15+
import { render, screen } from '@testing-library/react';
16+
import { describe, expect, it } from 'vitest';
17+
import { ChatbotEnhanced } from '@object-ui/plugin-chatbot';
18+
import { hydratedMessagesToChatMessages } from '../AiChatPage';
19+
import { detectConversationLanguage } from '../conversationLanguage';
20+
import {
21+
aiMessageRowsToServerMessages,
22+
toUIMessages,
23+
type RawAiMessageRow,
24+
} from '../../../hooks/useChatConversation';
25+
26+
const ASK = '把这条状态改成 Active';
27+
const CONTINUATION =
28+
'[HITL pa_8cf40802-901e-44ae-9610-bcaac8935d3b] The operator approved the `update_record` call. ' +
29+
'It executed successfully with result: {"status":"written","operation":"update","recordId":"vhGuAdkNThenRvKh"}. ' +
30+
"Acknowledge the outcome concisely and continue the user's task.";
31+
const FOLLOW_UP = 'Also set the owner to Li Lei';
32+
33+
function storedRows(extra: RawAiMessageRow[] = []): RawAiMessageRow[] {
34+
return [
35+
{ id: 'u1', role: 'user', content: ASK },
36+
{
37+
id: 'a1',
38+
role: 'assistant',
39+
content: '请在上方卡片确认。',
40+
tool_calls: JSON.stringify([
41+
{
42+
type: 'tool-call',
43+
toolCallId: 'c1',
44+
toolName: 'update_record',
45+
input: { objectName: 'jpph_customer', recordId: 'vhGuAdkNThenRvKh', values: { status: 'Active' } },
46+
},
47+
]),
48+
},
49+
{
50+
id: 't1',
51+
role: 'tool',
52+
tool_call_id: 'c1',
53+
content: JSON.stringify([
54+
{
55+
type: 'tool-result',
56+
toolCallId: 'c1',
57+
toolName: 'update_record',
58+
output: {
59+
type: 'text',
60+
value: JSON.stringify({ status: 'pending_approval', pendingActionId: 'pa_8cf40802-901e-44ae-9610-bcaac8935d3b' }),
61+
},
62+
},
63+
]),
64+
},
65+
{ id: 'u2', role: 'user', content: CONTINUATION },
66+
{ id: 'a2', role: 'assistant', content: '已将验收客户 R2 的状态改为 Active。' },
67+
...extra,
68+
];
69+
}
70+
71+
function reload(rows: RawAiMessageRow[]) {
72+
return hydratedMessagesToChatMessages(toUIMessages(aiMessageRowsToServerMessages(rows)));
73+
}
74+
75+
describe('a reloaded conversation hides the HITL continuation (objectstack#30223)', () => {
76+
it('hydrates the stored continuation and draws no bubble for it', () => {
77+
const messages = reload(storedRows());
78+
// Stored, and hydrated as a user message: the store has no other channel.
79+
expect(messages.some((m) => m.role === 'user' && m.content === CONTINUATION)).toBe(true);
80+
81+
render(<ChatbotEnhanced messages={messages} />);
82+
expect(screen.getByText(ASK)).toBeInTheDocument();
83+
expect(screen.getByText('已将验收客户 R2 的状态改为 Active。')).toBeInTheDocument();
84+
expect(screen.queryByText(/The operator approved/)).toBeNull();
85+
expect(screen.queryByText(/\[HITL /)).toBeNull();
86+
});
87+
88+
it('CONTROL: an ordinary stored user message still draws as the person\'s bubble', () => {
89+
render(
90+
<ChatbotEnhanced
91+
messages={reload(storedRows([{ id: 'u3', role: 'user', content: FOLLOW_UP }]))}
92+
/>,
93+
);
94+
expect(screen.getByText(FOLLOW_UP)).toBeInTheDocument();
95+
expect(screen.getByText(ASK)).toBeInTheDocument();
96+
});
97+
});
98+
99+
describe('the conversation-language probe reads past the HITL continuation (objectstack#30223)', () => {
100+
it('a Chinese conversation stays Chinese after an English continuation', () => {
101+
expect(
102+
detectConversationLanguage([
103+
{ role: 'user', content: ASK },
104+
{ role: 'assistant', content: '请确认。' },
105+
{ role: 'user', content: CONTINUATION },
106+
]),
107+
).toBe('zh-CN');
108+
});
109+
110+
it('CONTROL: an English turn the person typed still decides the language', () => {
111+
expect(
112+
detectConversationLanguage([
113+
{ role: 'user', content: ASK },
114+
{ role: 'user', content: FOLLOW_UP },
115+
]),
116+
).toBeUndefined();
117+
});
118+
});

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// pack's translation of them becomes reachable (objectui#3837 removed the one
1313
// label that had drifted in here).
1414

15+
import { isHitlContinuation } from '@object-ui/plugin-chatbot/core';
16+
1517
/** A message shape both the floating panel and the full-page chat can supply. */
1618
interface LangProbeMessage {
1719
role?: string;
@@ -49,6 +51,11 @@ export function detectConversationLanguage(
4951
if (!m || m.role !== 'user') continue;
5052
const text = userText(m);
5153
if (!text.trim()) continue;
54+
// objectstack#30223 — the English follow-up the chat sends the model after
55+
// 确认 / 取消 is not the person's turn. Read as one, it turned a Chinese
56+
// conversation's outbound text English whenever the action's result
57+
// carried no Chinese.
58+
if (isHitlContinuation(text)) continue;
5259
return /[一-鿿]/.test(text) ? 'zh-CN' : undefined;
5360
}
5461
return undefined;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#30223 — the conversation list (`/_console/ai/build/…` left rail)
5+
* previews the latest user message. The follow-up turn the chat sends the model
6+
* after 确认 / 取消 is stored as one, so the preview showed 「[HITL pa_…] The
7+
* operator approved …」. The preview now reads past it to what the person asked.
8+
*/
9+
10+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11+
import { renderHook, waitFor } from '@testing-library/react';
12+
13+
import { useConversationList } from '../useConversationList';
14+
15+
const API_BASE = 'http://ai.test/api/v1/ai';
16+
const ASK = '把这条状态改成 Active';
17+
const CONTINUATION =
18+
'[HITL pa_8cf40802-901e-44ae-9610-bcaac8935d3b] The operator approved the `update_record` call. ' +
19+
'It executed successfully with result: {"statu';
20+
21+
function jsonResponse(body: unknown): Response {
22+
return new Response(JSON.stringify(body), {
23+
status: 200,
24+
headers: { 'Content-Type': 'application/json' },
25+
});
26+
}
27+
28+
describe('useConversationList — the preview skips the HITL continuation (objectstack#30223)', () => {
29+
let fetchMock: ReturnType<typeof vi.fn>;
30+
31+
beforeEach(() => {
32+
fetchMock = vi.fn();
33+
vi.stubGlobal('fetch', fetchMock);
34+
});
35+
36+
afterEach(() => {
37+
vi.unstubAllGlobals();
38+
});
39+
40+
it('previews the person\'s last ask, not the continuation stored after it; an ordinary row is unchanged', async () => {
41+
fetchMock.mockResolvedValueOnce(
42+
jsonResponse({
43+
conversations: [
44+
{
45+
id: 'conv-confirmed',
46+
title: '修改客户状态',
47+
updatedAt: '2026-11-05T03:00:00.000Z',
48+
messages: [
49+
{ role: 'user', content: ASK },
50+
{ role: 'assistant', content: [{ type: 'text', text: '请在上方卡片确认。' }] },
51+
{ role: 'user', content: CONTINUATION },
52+
{ role: 'assistant', content: '已将状态改为 Active。' },
53+
],
54+
},
55+
{
56+
// CONTROL: no confirm in this conversation.
57+
id: 'conv-plain',
58+
title: '查询',
59+
updatedAt: '2026-11-05T02:00:00.000Z',
60+
messages: [
61+
{ role: 'user', content: 'How many leads?' },
62+
{ role: 'assistant', content: '12' },
63+
{ role: 'user', content: [{ type: 'text', text: 'And how many are open?' }] },
64+
],
65+
},
66+
{
67+
// Only a continuation and no typed ask to fall back to: no preview,
68+
// rather than the continuation.
69+
id: 'conv-only-continuation',
70+
title: '旧对话',
71+
updatedAt: '2026-11-05T01:00:00.000Z',
72+
messages: [{ role: 'user', content: CONTINUATION }],
73+
preview: CONTINUATION,
74+
},
75+
],
76+
}),
77+
);
78+
79+
const { result } = renderHook(() => useConversationList({ userId: 'u1', apiBase: API_BASE }));
80+
await waitFor(() => expect(result.current.conversations).toHaveLength(3));
81+
82+
const byId = new Map(result.current.conversations.map((c) => [c.id, c]));
83+
expect(byId.get('conv-confirmed')?.preview).toBe(ASK);
84+
expect(byId.get('conv-plain')?.preview).toBe('And how many are open?');
85+
expect(byId.get('conv-only-continuation')?.preview).toBeUndefined();
86+
});
87+
});

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { useCallback, useEffect, useState } from 'react';
1212
import { publishPlanApprovalPending } from '../assistant/assistantBus.js';
13+
import { isHitlContinuation } from '@object-ui/plugin-chatbot/core';
1314

1415
/**
1516
* One row of the conversation-history list.
@@ -76,15 +77,19 @@ function extractPreview(row: ServerConversation): string | undefined {
7677
const messages = row.messages;
7778
if (!messages || messages.length === 0) return undefined;
7879
// Prefer the most recent user message, fall back to last message.
80+
// objectstack#30223 — a HITL continuation (the turn the chat sends the model
81+
// after 确认 / 取消) is stored as a user message but is not the person's
82+
// words, so the preview reads past it to what they actually asked.
7983
for (let i = messages.length - 1; i >= 0; i--) {
8084
const m = messages[i];
8185
if (m.role === 'user') {
8286
const text = stringifyContent(m.content);
83-
if (text) return text;
87+
if (text && !isHitlContinuation(text)) return text;
8488
}
8589
}
8690
const last = messages[messages.length - 1];
87-
return stringifyContent(last?.content);
91+
const fallback = stringifyContent(last?.content);
92+
return isHitlContinuation(fallback) ? undefined : fallback;
8893
}
8994

9095
function stringifyContent(content: unknown): string | undefined {
@@ -122,7 +127,8 @@ function stringifyContent(content: unknown): string | undefined {
122127

123128
function normalize(row: ServerConversation): ConversationListItem {
124129
const title = row.title?.trim();
125-
const preview = extractPreview(row) ?? stringifyContent(row.preview);
130+
const served = stringifyContent(row.preview);
131+
const preview = extractPreview(row) ?? (isHitlContinuation(served) ? undefined : served);
126132
return {
127133
id: row.id,
128134
title: isPlaceholderTitle(title) ? undefined : title,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
type MessageProps,
4949
} from './elements/message';
5050
import { RecordLinkContext } from './recordLink';
51+
import { isHitlContinuation } from './hitlContinuation';
5152
import type { PublishHealth, PublishOutcome } from './publishHealth';
5253
import {
5354
PromptInput,
@@ -4533,6 +4534,11 @@ const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
45334534
) : (
45344535
messages.map((message) => {
45354536
const isUser = message.role === 'user';
4537+
// objectstack#30223 — the follow-up turn `useHitlInChat` sends
4538+
// the model after 确认 / 取消 is not the person's words, so it
4539+
// draws no bubble: live, after a reload, or on a shared
4540+
// transcript. The model still received it.
4541+
if (isUser && isHitlContinuation(message.content)) return null;
45364542
const tools = message.toolInvocations ?? [];
45374543
const reasoning = message.reasoning?.trim();
45384544
const sources = message.sources ?? [];

0 commit comments

Comments
 (0)