diff --git a/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.i18n.yaml new file mode 100644 index 0000000000..8cfbcd08c3 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md +2026-07-31-drop-user-message-edit-stub.md: 5a34be1dd285f4aced9e6cfb2e324b20ec734bed +2026-07-31-drop-user-message-edit-stub.zh.md: 768cb4618b4a85f61e6348f8c6fb6a93d686e7d3 diff --git a/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md new file mode 100644 index 0000000000..5a34be1dd2 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md @@ -0,0 +1,27 @@ +# Agent Note: Drop the user-message edit stub + +Status: implemented + +English | [中文](2026-07-31-drop-user-message-edit-stub.zh.md) + +## Problem + +The user bubble's IconActions row carried an edit button beside copy and branch. Nothing backed it: the control had no click handler, no client mutation, and no host operation for resending an edited message. A user who found it saw an affordance the product cannot honor. + +## Decision + +`MessageIconActions` renders clock / copy / branch only, and its `edit` prop is gone with the button; `MessageItem` no longer passes it. The user bubble and the assistant chrome now differ only by clock side. The package README records the missing capability under Known Limitations, and the web message-actions golden pins the row without the control. + +The common locale keeps its generic `edit` term, which is shared vocabulary rather than this component's copy. + +Reintroduce the control together with the capability: a client mutation that edits a settled user message and the host behavior that decides what the edited message does to the turn that already consumed it. + +## Alternatives considered + +**Disable the button with a tooltip.** A visible-but-dead control still advertises editing and costs the same explaining; removal is the honest state. + +**Wire it to the queue editor.** The queue edits a message that has not been sent. A settled user message is already in the transcript and in the model's context, so reusing that editor would silently mean something else. + +## Consequences + +Web offers no way to correct a sent message; branching from the message is the nearest available gesture. Reintroduction is a UI-only change once the mutation exists, since the row composes its actions from props. diff --git a/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.zh.md b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.zh.md new file mode 100644 index 0000000000..768cb4618b --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.zh.md @@ -0,0 +1,27 @@ +# Agent Note: 移除 user 消息的编辑存根 + +Status: implemented + +[English](2026-07-31-drop-user-message-edit-stub.md) | 中文 + +## 问题 + +user 气泡的 IconActions 行在复制和分支旁边还有一个编辑按钮,但其背后什么都没有:该控件没有点击处理、没有 client 侧变更,也没有 host 侧重新发送已编辑消息的操作。用户找到它时,看到的是一个产品无法兑现的可供性。 + +## 决策 + +`MessageIconActions` 只渲染时钟/复制/分支,其 `edit` prop 随按钮一并删除;`MessageItem` 不再传入该 prop。现在 user 气泡与 assistant chrome 只在时钟位置上不同。包 README 在 Known Limitations 中记录这项缺失的能力,web 的 message-actions 金样固定了不含该控件的动作行。 + +公共 locale 保留通用的 `edit` 词条:它是共享词汇,而非本组件的文案。 + +重新引入该控件时要与能力一起落地:既需要编辑已定稿 user 消息的 client 变更,也需要 host 侧决定这条编辑后的消息对已经消费过它的轮次意味着什么。 + +## 曾考虑的替代方案 + +**把按钮置灰并加提示。** 一个可见但无效的控件仍在宣告可以编辑,解释成本相同;直接移除才是诚实的状态。 + +**接到队列编辑器上。** 队列编辑的是尚未发送的消息。已定稿的 user 消息已经进入转录和模型上下文,复用该编辑器会让同一个动作悄悄变成另一件事。 + +## 后果 + +Web 没有任何途径修正已发送的消息;从该消息分支是最接近的现有手势。由于动作行的内容完全由 props 组合而来,能力就绪后重新引入只是一次纯 UI 改动。 diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index 4d798e11ed..bb58ccefc4 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -64,14 +64,14 @@ describe('web e2e: message IconActions and clocks on settled history', () => { await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBe(1) // Focus-reveal the footers (hover:hover keeps them opacity-hidden until - // hover/focus-within). User has three actions; each turn's last content - // assistant has copy + branch. + // hover/focus-within). User and each turn's last content assistant both + // have copy + branch. const copyButtons = page.getByRole('button', { name: 'Copy' }) await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2) await copyButtons.first().focus() await expect.poll(() => page.getByRole('button', { name: 'Branch into a new conversation' }).count(), { timeout: 5_000 }) .toBeGreaterThanOrEqual(2) - await expect.poll(() => page.getByRole('button', { name: 'Edit' }).count(), { timeout: 5_000 }).toBe(1) + await expect.poll(() => page.getByRole('button', { name: 'Edit' }).count(), { timeout: 5_000 }).toBe(0) }, 60_000) it.skipIf(MODE === 'record')('matches the conversation aria golden with IconActions and clocks', async () => { diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index 2d075a1dea..10efaa2b94 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index 9bd145dd20..d5ec43f45a 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index 50109a1515..cb3032ec89 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index c9f087796d..a01f63d018 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 515b6be2de..3333237798 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 40c9cd0395..c65690aa73 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md index f79b30678d..b442b4345a 100644 --- a/apps/web/tests/snapshots/live-interactions/loading.expected.md +++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index c79c8033e9..35b841e05c 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/message-actions/ui.expected.md b/apps/web/tests/snapshots/message-actions/ui.expected.md index 73b7637400..34ce19a69f 100644 --- a/apps/web/tests/snapshots/message-actions/ui.expected.md +++ b/apps/web/tests/snapshots/message-actions/ui.expected.md @@ -10,8 +10,6 @@ - tooltip "Copy" - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - img - img diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md index f494959f46..8f62c3a297 100644 --- a/apps/web/tests/snapshots/plan-review/approved.expected.md +++ b/apps/web/tests/snapshots/plan-review/approved.expected.md @@ -10,8 +10,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index 307dbc8094..117c539d75 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md index 4e73ad1320..829f21a70f 100644 --- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md +++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index 4a0628b312..169dde2c51 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index 72af6803cb..e2c91f7584 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/seeded-history/command-row.expected.md b/apps/web/tests/snapshots/seeded-history/command-row.expected.md index 948d5579de..205f58cdd0 100644 --- a/apps/web/tests/snapshots/seeded-history/command-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/command-row.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - img - img diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index cd74c1053c..747399e414 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - img - img diff --git a/apps/web/tests/snapshots/steering/mid-steer.expected.md b/apps/web/tests/snapshots/steering/mid-steer.expected.md index 0134e6efb9..b41f6baae2 100644 --- a/apps/web/tests/snapshots/steering/mid-steer.expected.md +++ b/apps/web/tests/snapshots/steering/mid-steer.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index bea75fac72..e412599acd 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md index 514aaf6219..372ed8da25 100644 --- a/apps/web/tests/snapshots/web-search-round/ui.expected.md +++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md @@ -9,8 +9,6 @@ - img - button "Branch into a new conversation": - img -- button "Edit": - - img - button "Context injection": - img - img diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index cffb1cc453..4aae948a04 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md -README.md: b183aab56a10c3a22eeab31f8b1b0e6b5219beba -README.zh.md: af6427774a048de324d57bc9856f77547e9a03cc +README.md: 7f428fd50411a8ee3b8e7e133d6465a77681900e +README.zh.md: e591072eb95ed461bb26b6636f00e24ab7ad2dfa diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index b183aab56a..7f428fd504 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -47,6 +47,7 @@ None; this package neither assembles nor sends a provider request. - **Stats-line durations cover the in-window flow only** — LLM and tool wall times fold the snapshot's assistant `timing` and tool call/result pairs, so nodes outside the loaded event window (older history) are not counted. - **Details panel is the minimal form and currently has no entry point** — selected call args/result raw display; the Input/Output/Metadata switch, Prev/Next stepping, and See-in-trajectory deep link are deferred. Tool rows stopped being details-panel click targets and nothing replaced that gesture, so `ChatViewInjected.openDetails` is implemented but uncalled and the panel (including its terminal card) is unreachable in the assembled application; its rendering stays covered by mounting it with a selection directly. - **Assistant per-message paging is a reserved slot** — drawn in the design, not implemented. The finalized content IconActions row (copy / branch / clock) ships under the last content-text assistant of each turn only; mid-turn narration and Think-only nodes stay chrome-free. Branch forks through the turn containing that message, increments the inherited title on the client, and then opens the child, while a fork or rename failure leaves the source selected. +- **Sent user messages cannot be edited** — the user bubble's IconActions row carries clock / copy / branch only, and branching from the message is the nearest gesture. The control returns with the capability behind it: a client mutation over a settled user message, plus the host behavior for the turn that already consumed it ([decision](../../../.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md)). - **The sparkle icon for the others tool row is a hand-drawn approximation** — the design glyph's vector geometry is not exportable locally; promotion into ui-primitives waits on an exact export. - **The approval panel's "Always allow this type" is deferred** — durable grants need a grant-storage design; only allow-once/reject answer today. - **TodoPanel truncates long item text to one ellipsized line** — the figma strip has no wrap or expand affordance; full text is not readable inline. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index af6427774a..e591072eb9 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -47,6 +47,7 @@ todo 两个面就是在该形状上的两个注册项,都是普通注册方插 - **统计行的耗时只覆盖窗口内消息流**:LLM(大语言模型)与工具墙钟时间由快照的 assistant `timing` 与工具 call/result 配对折算,落在已加载事件窗口之外的节点(更早的历史)不计入。 - **详情面板是最小形态,且当前没有入口**:以原始形式显示已选择调用的参数/结果;Input/Output/Metadata 切换、Prev/Next 步进与 See-in-trajectory 深链接暂缓实现。工具行已不再是详情面板的点击目标,且没有任何手势接替它,因此 `ChatViewInjected.openDetails` 虽已实现却无人调用,该面板(含其终端卡片)在组装后的应用中不可达;其渲染仍由直接以选中态挂载它来覆盖。 - **assistant 逐消息分页是预留 slot**:设计中已有图稿,尚未实现。已定稿的内容 IconActions 行(复制/分支/时钟)只挂在每个轮次中最后一条带 text 内容的 assistant 下;轮次中间的叙述与纯 Think 节点不带 chrome。分支会 fork 到包含该消息的轮次末尾,在 client 端递增继承标题后打开子会话,而 fork 或改名失败时源会话保持选中。 +- **已发送的 user 消息无法编辑**:user 气泡的 IconActions 行只有时钟/复制/分支,从该消息分支是最接近的手势。该控件要与其背后的能力一起回归:既需要针对已定稿 user 消息的 client 变更,也需要 host 侧对已经消费过它的轮次给出行为([决策](../../../.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md))。 - **others 工具行的闪光图标是手绘近似版本**:无法在本地导出设计字形的矢量几何;等到存在精确导出后再将其提升到 ui-primitives。 - **审批面板的「始终允许此类」暂缓**:持久授权需要授权存储设计;今天只能回答允许一次/拒绝。 - **TodoPanel 将过长条目截成单行省略号**:figma 条没有换行或展开入口,完整文本无法在行内读完。 diff --git a/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx index dbb8a8628e..cd6b0a36ce 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx @@ -1,10 +1,9 @@ // Shared IconActions chrome for user and assistant messages: copy live, -// branch wired through onBranch, date-aware clock, -// optional edit stub. +// branch wired through onBranch, date-aware clock. import { useCallback } from 'react' import { - IconBranchOutline16, IconCopyOutline16, IconEditOutline16, Tooltip, + IconBranchOutline16, IconCopyOutline16, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatViewSlotProps } from '../contract/slots.ts' import { formatMessageClock, writeClipboard } from './message-chrome.ts' @@ -18,8 +17,6 @@ export interface MessageIconActionsProps { time: number /** Clock before icons (user) or after (assistant). */ clock: 'start' | 'end' - /** When true, append the stub edit control (user bubble). */ - edit?: boolean | undefined /** Fork the session at this message. */ onBranch?: (() => void) | undefined /** Parent layout class composed onto the actions row. */ @@ -30,11 +27,11 @@ export interface MessageIconActionsProps { /** * Copy / branch (/ clock) IconActions row shared by user and assistant chrome. - * @param props - Copy text, event time, clock side, optional edit, branch callback, className. + * @param props - Copy text, event time, clock side, branch callback, className. * @returns The actions row element. */ export function MessageIconActions({ - text, time, clock, edit, onBranch, className, t, + text, time, clock, onBranch, className, t, }: MessageIconActionsProps) { const day = useCalendarDay() const onCopy = useCallback(() => { @@ -58,13 +55,6 @@ export function MessageIconActions({ - {edit === true && ( - - - - )} {clock === 'end' ? clockEl : null} ) diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx index d271db9989..64f47b2f7b 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx @@ -1,5 +1,5 @@ // MessageItem: simple chat nodes — user bubble (right-aligned, with -// clock + copy / branch / edit IconActions), steering (badged bubble), context +// clock + copy / branch IconActions), steering (badged bubble), context // injection, retry disclosure, and unknown-surface JSON rows. import { memo, useEffect, useMemo, useState } from 'react' @@ -156,7 +156,6 @@ export const MessageItem = memo(function MessageItem({ text={text} time={node.time} clock="start" - edit onBranch={onFork === undefined ? undefined : () => { onFork(node.seq) }} className={css.actions} t={t} diff --git a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx index 779b925fd9..1d0fb3ebc5 100644 --- a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx @@ -27,7 +27,7 @@ afterEach(() => { const t: MessageItemProps['t'] = makeTranslate(zh, commonZh) describe('MessageItem arms', () => { - it('user bubbles expose clock / copy / branch / edit; copy writes the text', () => { + it('user bubbles expose clock / copy / branch and no edit; copy writes the text', () => { const writeText = vi.fn().mockResolvedValue(undefined) Object.defineProperty(navigator, 'clipboard', { configurable: true, @@ -47,7 +47,7 @@ describe('MessageItem arms', () => { expect(screen.getByText('14:24')).toBeTruthy() expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() expect(screen.getByRole('button', { name: '在新对话中分支' })).toBeTruthy() - expect(screen.getByRole('button', { name: '编辑' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '编辑' })).toBeNull() fireEvent.click(screen.getByRole('button', { name: '复制' })) expect(writeText).toHaveBeenCalledWith('hello bubble') })