Merge pull request #4186 from deepseek-harness/xtr/revert-3880-chat-behavior
fix(chat): revert Chat behavior changes from #3880
This commit is contained in:
commit
11c4c52d1a
22 files changed
+168
-89
No files matched your search
+2
-2
@@ -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 .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md
|
||||
2026-09-01-v2-embedded-assistant-streams.md: 219879600f8435a6ecc9593661dd3f463ed1158c
|
||||
2026-09-01-v2-embedded-assistant-streams.zh.md: d38c180b1f1de689620f2e0533d2b02f4a71e466
|
||||
2026-09-01-v2-embedded-assistant-streams.md: 89b494959fcc10a2c9847908adcf13d5eedcd749
|
||||
2026-09-01-v2-embedded-assistant-streams.zh.md: 63cb425740cd2243361ec00a6dadad82c38f6f0f
|
||||
@@ -31,7 +31,7 @@ The migration publication verifier and frozen v2 fixture validator require the e
|
||||
|
||||
The Web follow adapter opts into these process-local frames and adds the last durable sequence observed at each start. It presents chunks as Client-only `assistant/live-chunk` updates between durable cursors, stages only a later matching settlement until the committed end, and reopens follow on a revision gap. A committed end publishes a named settlement delta that removes the attempt's transient matches, adds the durable entry, and replays only affected Conversation Contexts; an abandoned end publishes the same delta without an entry. A reconnect baseline carries the active attempt's durable start cursor and compact prefix.
|
||||
|
||||
The Client event source passes durable settlements through unchanged. The Chat and Trajectory Assistant nodes fold `assistant/live-chunk` while an attempt is active, build settled output directly from `assistant/message`, and do not replay an `assistant/attempt` stream for presentation. Cold settled presentation therefore does not reconstruct per-token timing; other consumers may expand the durable stream when they require its exact evidence.
|
||||
The Client event source passes durable settlements through unchanged. Chat and Trajectory fold `assistant/live-chunk` while an attempt is active and build settled output directly from `assistant/message`. Chat does not reconstruct first-token timing after settlement retires the transient chunks. Trajectory reads timing from the [compact stream records](2026-09-06-embedded-stream-record-readers.md) in `assistant/message` and `assistant/attempt`, including when opening history. Neither target expands settled streams into per-delta objects for presentation; other consumers may expand the durable stream when they require its exact evidence.
|
||||
|
||||
### Released v1 to v2 migration
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ Migration publication verifier 与冻结的 v2 fixture validator 要求嵌入式
|
||||
|
||||
Web follow adapter 显式选择接收这些进程本地 frame,并为每个 start 补充当时观察到的最后一个持久序号。它把 chunk 呈现为持久 cursor 之间的 Client-only `assistant/live-chunk` update,只暂存 start 之后匹配的 settlement,并在 revision 缺口时重新打开 follow。committed end 会发布具名 settlement delta,删除该 attempt 的 transient match、加入持久 entry,并只重放受影响的 Conversation Context;abandoned end 会发布不含 entry 的同类 delta。重连 baseline 携带活跃 attempt 的持久起始 cursor 与紧凑前缀。
|
||||
|
||||
Client event source 原样传递持久 settlement。Chat 与 Trajectory 的 Assistant node 在 attempt 活跃期间折叠 `assistant/live-chunk`,直接从 `assistant/message` 构建 settled output,并且不为展示重放 `assistant/attempt` stream。因此冷恢复的 settled presentation 不会重建逐 token timing;其他消费方需要精确证据时仍可展开持久 stream。
|
||||
Client event source 原样传递持久 settlement。Chat 与 Trajectory 在 attempt 活跃时折叠 `assistant/live-chunk`,直接从 `assistant/message` 构造 settled output。结算移除临时 chunk 后,Chat 不会重建首 token 计时。Trajectory 从 `assistant/message` 与 `assistant/attempt` 中的[紧凑流记录](2026-09-06-embedded-stream-record-readers.zh.md)读取计时,包括打开历史时。两个目标都不会为展示将已结算流展开为逐 delta 对象;其他消费方需要精确证据时仍可展开持久 stream。
|
||||
|
||||
### 已发布 v1 到 v2 迁移
|
||||
|
||||
|
||||
+2
-2
@@ -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 .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md
|
||||
2026-09-06-embedded-stream-record-readers.md: 76e109de577d070093bb7123aa50d9a4ae7fcbe5
|
||||
2026-09-06-embedded-stream-record-readers.zh.md: 33f4b31117197c47e7f2ff1637bc13fa2cbf1a8c
|
||||
2026-09-06-embedded-stream-record-readers.md: efad86a4f940bf3b56d4b141a8fe1e3659c8dd9c
|
||||
2026-09-06-embedded-stream-record-readers.zh.md: f7131a27171163954bca8b7013dc6d90f907c552
|
||||
@@ -20,7 +20,7 @@ After v2 embedded streams settlement widened with the message content and Chat a
|
||||
- Run readers: `runFirstTokenTime` and `runFirstVisibleTime` reconstruct the first qualifying member's time from `time0` and the `dt` gaps and stop scanning there; a name-bearing Tool-call run yields `time0` without reading a fragment.
|
||||
- Stream readers: `assistantStreamFirstTokenTime`, `assistantStreamHasVisibleContent`, `assistantStreamHasVisibleText`, `lastAssistantStreamChunk(stream, type)` (backward scan), `assistantStreamChunks(stream, type)`, `joinAssistantStreamText`, and `assembleAssistantStream`, which feeds a `BlockAssembler` one joined delta per run (assembly only concatenates, so blocks, usage, finish, and replay state equal the per-member result). `RawStreamChunkType` excludes the delta types, so a raw-chunk lookup can never silently skip packed members.
|
||||
|
||||
Session Stats, Chat, and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Chat and Trajectory settle content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
|
||||
Session Stats and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Trajectory settles content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. Chat follows its [settled-reply timing policy](../bug-fix/2026-09-14-chat-presentation-defaults.md). The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
|
||||
|
||||
`expandAssistantStream` keeps its strict validation and its remaining callers, which need every member or validate the stream at a durable boundary: Session restore validation, the v1-to-v2 migration validator and publication Worker replay, the reconnect baseline, and test support.
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Session 格式 v2 将每次模型尝试的紧凑流(`AssistantStreamRecord[]`
|
||||
- Run 读取器:`runFirstTokenTime` 与 `runFirstVisibleTime` 从 `time0` 与 `dt` 间隔重建首个合格成员的时间并停止扫描;带名称的 Tool-call run 直接产出 `time0`,不读片段。
|
||||
- 流读取器:`assistantStreamFirstTokenTime`、`assistantStreamHasVisibleContent`、`assistantStreamHasVisibleText`、`lastAssistantStreamChunk(stream, type)`(逆向扫描)、`assistantStreamChunks(stream, type)`、`joinAssistantStreamText` 与 `assembleAssistantStream`(每个 run 向 `BlockAssembler` 喂入一个拼接后的 delta;组装只做拼接,因此 blocks、usage、finish 与 replay state 与逐成员结果一致)。`RawStreamChunkType` 排除 delta 类型,因此原始 chunk 查找不可能静默跳过打包成员。
|
||||
|
||||
Session Stats、Chat 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Chat 与 Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
|
||||
Session Stats 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。Chat 遵循[已结算回复的计时策略](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
|
||||
|
||||
`expandAssistantStream` 保留其严格校验与其余调用方(需要每个成员或在持久边界校验流):Session 恢复校验、v1-to-v2 迁移校验器与发布 Worker 重放、重连基线、测试支撑。
|
||||
|
||||
|
||||
@@ -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/bug-fix/2026-09-14-chat-presentation-defaults.md
|
||||
2026-09-14-chat-presentation-defaults.md: 6703a14e2ad108c4b43dd50692a44c159cf3ea5a
|
||||
2026-09-14-chat-presentation-defaults.zh.md: 67ca433339ecf6e44d3f74b6c3ff9e465f802b0b
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Keep Chat presentation independent of Trajectory inspection
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-14-chat-presentation-defaults.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Trajectory inspection benefits from exposing complete recorded reasoning. Applying that default to Chat expands the live transcript during reasoning and changes its height when an answer or Tool call arrives. The Trajectory inspection change also added historical first-token recovery to Chat without a separate Chat behavior decision.
|
||||
|
||||
## Decision
|
||||
|
||||
[Chat](../../../../packages/client/ui-chat/README.md#turn-process-folding) starts each reasoning row collapsed and retains the reader's manual disclosure choice through subsequent output and settlement. Settlement retires the observed live chunks and rebuilds Chat reply nodes from durable events without recovering first-token time from embedded streams. Consequently, completed-turn TTFT and decoding speed are absent after live settlement as well as after reopening history. Turn-level process folding remains independently owned.
|
||||
|
||||
[Trajectory inspection](../feature/2026-09-09-ptc-trajectory-code-inspection.md) keeps its expanded reasoning default, recorded timing, JSON controls, and PTC code inspector. The [compact stream readers](../architecture/2026-09-06-embedded-stream-record-readers.md) remain available to Trajectory and other consumers. These decisions partially supersede the Chat presentation additions while preserving both notes' independent rationale.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep Chat's automatic expansion and historical timing recovery.** These change Chat behavior beyond the requested Trajectory inspection work. Reintroducing either requires a separate Chat product decision and its own verification.
|
||||
|
||||
**Revert the entire inspection change.** That would remove the requested Trajectory behavior along with the unintended Chat changes.
|
||||
|
||||
## Consequences
|
||||
|
||||
Chat reasoning requires a click to inspect in full. Elapsed turn time and the independently projected Session Stats remain available. Assembler tests distinguish transient retirement at live settlement from reopening durable history; browser replay verifies the completed-turn timing dialog before and after reload. Component tests cover collapsed streaming and reasoning-only replies and manual disclosure across answer and Tool-call arrival. Trajectory tests retain its separate defaults and timing.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 保持 Chat 展示与 Trajectory 查看独立
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-14-chat-presentation-defaults.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Trajectory 查看适合直接展示完整的已记录思考。将该默认行为应用到 Chat 会在思考期间展开实时对话,并在回答或工具调用到来时改变其高度。Trajectory 查看改动还为 Chat 增加了历史首 token 计时恢复,却没有独立的 Chat 行为决策。
|
||||
|
||||
## 决策
|
||||
|
||||
[Chat](../../../../packages/client/ui-chat/README.zh.md#turn-process-folding) 的每个思考行初始都折叠,后续输出和结算保留用户手动选择的展开状态。结算会移除观测到的实时 chunk,并从持久事件重建 Chat 回复节点,不从内嵌流恢复首 token 时间。因此,实时结算后和重新打开历史后,已完成轮次都不显示 TTFT 和解码速度。轮次级过程折叠仍独立维护。
|
||||
|
||||
[Trajectory 查看](../feature/2026-09-09-ptc-trajectory-code-inspection.zh.md) 保留思考默认展开、已记录计时、JSON 控件和 PTC 代码查看器。[紧凑流读取器](../architecture/2026-09-06-embedded-stream-record-readers.zh.md) 仍供 Trajectory 和其他消费方使用。这些决策部分取代了 Chat 展示增量,同时保留两份记录各自独立的理由。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留 Chat 自动展开和历史计时恢复。** 这些改变了所请求的 Trajectory 查看工作之外的 Chat 行为。重新引入任一行为都需要独立的 Chat 产品决策及相应验证。
|
||||
|
||||
**撤回整个查看改动。** 这会在撤回意外 Chat 改动的同时移除所请求的 Trajectory 行为。
|
||||
|
||||
## 影响
|
||||
|
||||
Chat 思考需要点击才能查看全文。轮次总耗时和独立投影的 Session Stats 仍可用。组装器测试区分实时结算时移除临时数据与重新打开持久历史;浏览器回放验证重载前后的已完成轮次计时对话框。组件测试覆盖流式与仅思考回复的默认折叠,以及回答和工具调用到来时的手动展开状态。Trajectory 测试保留其独立默认值和计时。
|
||||
+2
-2
@@ -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 .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md
|
||||
2026-09-09-ptc-trajectory-code-inspection.md: 477d199f519b5d515e5d58430bd902d9d209e46b
|
||||
2026-09-09-ptc-trajectory-code-inspection.zh.md: f305569176214c63ac549b0ec5103289e3e57a88
|
||||
2026-09-09-ptc-trajectory-code-inspection.md: 19779d20e8d08ce0ca9678ab6626765faa13cf65
|
||||
2026-09-09-ptc-trajectory-code-inspection.zh.md: 7cdb2321d61893e43426798d311847662450d289
|
||||
@@ -16,6 +16,8 @@ The result view preserves recorded text and uses a tree only for complete JSON o
|
||||
|
||||
The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlement; the [client presentation decision](../architecture/2026-08-23-client-derived-tool-presentation.md) still owns deriving UI from recorded facts. This inspector adds no Session events or host presentation fields.
|
||||
|
||||
Trajectory thinking opens by default and supports manual disclosure. [Chat disclosure defaults](../bug-fix/2026-09-14-chat-presentation-defaults.md) are a separate presentation decision.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep source inside the argument tree.** JSON escaping obscures program structure and makes copying executable source cumbersome.
|
||||
@@ -26,4 +28,4 @@ The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlemen
|
||||
|
||||
## Consequences
|
||||
|
||||
Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover body arrival, manual disclosure, and switching records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.
|
||||
Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover manual disclosure and switching Trajectory records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.
|
||||
@@ -16,6 +16,8 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
|
||||
|
||||
[PTC 运行时决策](2026-06-15-ptc.zh.md) 仍负责执行与结算;[客户端展示决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md) 仍负责从已记录事实派生 UI。此检查器不增加 Session 事件或宿主展示字段。
|
||||
|
||||
Trajectory 思考默认展开,并支持手动展开折叠。[Chat 展开默认值](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)是独立的展示决策。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把源码保留在参数树中。** JSON 转义遮蔽程序结构,也使复制可执行源码变得繁琐。
|
||||
@@ -26,4 +28,4 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
|
||||
|
||||
## 后果
|
||||
|
||||
读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖正文到达、手动展开折叠及记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。
|
||||
读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖手动展开折叠及 Trajectory 记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。
|
||||
@@ -291,10 +291,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
await input.press('Enter')
|
||||
if (MODE !== 'record') {
|
||||
const thinking = page.locator('[data-variant="think"][data-state="running"]')
|
||||
const disclosure = thinking.getByRole('button')
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true')
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
await expect.poll(() => thinking.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
const liveTail = thinking.locator('[data-follow-end]')
|
||||
await expect.poll(async () => {
|
||||
if (await liveTail.count() !== 1) return false
|
||||
|
||||
@@ -17,7 +17,7 @@ import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
@@ -201,13 +201,22 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
||||
await timeTrigger.click()
|
||||
const timeDialog = page.getByRole('dialog', { name: 'Turn time and speed' })
|
||||
expect(await timeDialog.count()).toBe(1)
|
||||
expect(await timeDialog.getByText(/tok\/s/).count()).toBeGreaterThan(0)
|
||||
expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(1)
|
||||
expect(await timeDialog.getByText(/tok\/s/).count()).toBe(0)
|
||||
expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(0)
|
||||
await page.keyboard.press('Escape')
|
||||
await trigger.click()
|
||||
|
||||
const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(USAGE_EXPANDED_EXPECTED, expanded, MODE)
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await expect.poll(() => timeTrigger.count(), { timeout: 15_000 }).toBe(1)
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await timeTrigger.click()
|
||||
expect(await timeDialog.count()).toBe(1)
|
||||
expect(await timeDialog.getByText(/tok\/s/).count()).toBe(0)
|
||||
expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
@@ -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-chat/README.md
|
||||
README.md: 254c511a023aa8217b73cc0fe3802f13f2a63d80
|
||||
README.zh.md: d79f8b5060bd6a452ddaf018074adaea88b85604
|
||||
README.md: c7c9fb7cb2a9a149fae9b27db78e019d821dfb95
|
||||
README.zh.md: 386d7aae91b157a7b09fcd0515bfd8d075a39638
|
||||
@@ -41,7 +41,7 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a
|
||||
|
||||
A completed Turn shows an expandable usage row only when the loaded window includes `turn/start` and every started model attempt reports safe, exact usage. The row omits unavailable optional buckets. Incomplete or contradictory accounting hides the complete disclosure instead of presenting a partial total.
|
||||
|
||||
Reply timing survives reopening history: recorded token timestamps supply first-token latency and generation duration, including the Step's first token from an earlier retry attempt. Missing Step starts or token timestamps leave the affected metrics unavailable.
|
||||
After Assistant replies settle, the completed-turn timing dialog omits TTFT and decoding speed, both after live replies and after reopening history. Elapsed turn time remains available. The Session Stats pill reads timing independently from its durable projection.
|
||||
|
||||
<a id="completed-turn-footer"></a>
|
||||
## Completed-turn footer
|
||||
@@ -53,6 +53,8 @@ The completed-turn action footer starts 20px below the preceding prose or extens
|
||||
<a id="turn-process-folding"></a>
|
||||
## Turn Process Folding
|
||||
|
||||
Each reasoning row starts collapsed, including during streaming and in reasoning-only replies. Clicking the row opens or closes its complete text; incoming answer text, Tool calls, and stream completion preserve that choice. The collapsed summary follows the latest reasoning line while streaming and shows the first line after settlement.
|
||||
|
||||
Settings → General exposes a persisted, localized `Normal` / `Compact` conversation-display preference in the `ui-chat` namespace; `Compact` is the default. Normal leaves process rows visible and renders no Turn-process control. In Compact mode, the System prompt remains independently visible before the opening User throughout the Turn. Context injection, reasoning, Assistant material, Tool rows, and Retry rows remain expanded while a Turn is open. At `turn/end`, its latest Step becomes the final-answer boundary only when it contains non-blank text, an image, or an unknown visible block—and no Tool-call block; preceding Context injection, reasoning, earlier Assistant material, Tool rows, and Retry rows then collapse by default. The control reports Turn-wide durable counts for non-subagent Tool calls, reply-bearing Assistant messages before the final answer, and subagent delegation calls; zero-valued segments are omitted, the Tool and subagent figures are mutually exclusive, and neither System prompt nor Context injection contributes a count. When all three counts are zero, the process still folds and the control reads `Thought for a while`. A full-width divider below the summary separates it from the answer or expanded process rows. User and steering messages, System prompt, error, max-token, and turn-tail rows stay outside, and a closed Turn with no final answer keeps all process evidence visible. A newly available process control is inserted without changing the relative order of existing rows: opening human input precedes the control and process rows from their first projection, while System prompt remains above that input. While older history remains available through Load earlier, process controls stay absent and no members are hidden; once history is complete, every eligible closed Turn uses the collapsed default immediately. Stable Chat Node Seats keep every renderer mounted, hidden members add no flow spacing, and a closed control sits 8px above its answer only when no independent input intervenes. Completion collapse does not depend on tail-follow position, so a reader above the tail may see the transcript reflow. An automatic collapse that would hide keyboard focus keeps the group open and leaves focus in place; a manual close focuses the process control before hiding its members. The session-scoped store records only manually expanded Turn-and-answer-Step generations; a different answer generation starts collapsed.
|
||||
|
||||
-----
|
||||
|
||||
@@ -43,7 +43,7 @@ kind: "package-reference"
|
||||
|
||||
只有当已加载窗口包含 `turn/start`,且每次已启动的模型尝试都报告安全、精确的用量时,已完成轮次才显示可展开的用量行。该行会省略不可用的可选用量桶。记账不完整或相互矛盾时,整个详情都不显示,避免把部分总量冒充完整结果。
|
||||
|
||||
重新打开历史后仍保留回复计时:记录中的 token 时间戳提供首 token 延迟和生成耗时,包括步骤中更早重试尝试的首个 token。缺少步骤起点或 token 时间戳时,受影响的指标保持不可用。
|
||||
Assistant 回复结算后,已完成轮次的计时对话框不显示 TTFT 和解码速度,实时回复完成后和重新打开历史后都如此。轮次总耗时仍可用。Session Stats 药丸从其持久化投影独立读取计时。
|
||||
|
||||
<a id="completed-turn-footer"></a>
|
||||
## 已完成轮次的页脚
|
||||
@@ -55,6 +55,8 @@ kind: "package-reference"
|
||||
<a id="turn-process-folding"></a>
|
||||
## 轮次过程折叠
|
||||
|
||||
每个思考行初始都折叠,包括流式输出期间和仅含思考的回复。点击该行可展开或收起完整文本;后续回答文本、工具调用和流结束都保留用户的选择。折叠摘要在流式输出期间跟随最新思考行,结算后显示第一行。
|
||||
|
||||
「设置 → 通用设置」提供持久化到 `ui-chat` 命名空间的「标准」/「紧凑」对话显示偏好,默认使用「紧凑」。标准模式保持所有过程行可见且不渲染轮次过程控件。紧凑模式下,系统提示词在整个轮次中始终独立显示于开场 User 上方。轮次打开期间,上下文注入、推理、Assistant 内容、工具行与重试行始终展开。到 `turn/end` 时,最后一个步骤只有在包含非空文本、图片或未知可见块且不含工具调用块时才成为最终答案边界;边界之前的上下文注入、推理、较早 Assistant 内容、工具行与重试行随后默认收起。控件展示覆盖整个轮次的非 subagent 工具调用数、最终答案之前带回复内容的 Assistant 消息数和 subagent 委派数;值为 0 的分段省略,工具调用与 subagent 两项互斥,系统提示词与上下文注入都不增加计数。三项全为 0 时过程仍会收起,控件标题显示「已思考」(英文为 `Thought for a while`)。摘要下方的通栏分隔线将其与正文或展开后的过程行隔开。用户与 steering 消息、系统提示词、错误、最大 token 与 turn-tail 行留在过程组外;关闭时没有最终答案的轮次保留全部过程证据。新的过程控件插入时不会改变既有行的相对顺序:开场人工输入从首次投影起便位于控件和过程行之前,系统提示词则始终位于该输入上方。只要仍可通过「加载更早」获取历史,过程控件就不出现,也不会隐藏任何成员;历史加载完整后,每个合格的已关闭轮次立即使用默认收起状态。稳定 Chat Node Seat 会让每个 renderer 保持挂载,隐藏成员不产生消息流间距;只有中间没有独立输入时,收起控件才与正文相隔 8px。完成后的收起不依赖是否跟随尾部,因此正在上方阅读的用户可能看到 transcript 重排。若自动收起会隐藏当前键盘焦点,则过程组保持展开且焦点留在原处;手动收起会先把焦点移到过程控件,再隐藏成员。会话作用域存储只记录用户手动展开的「轮次 + 正文步骤」generation;不同正文 generation 默认收起。
|
||||
|
||||
-----
|
||||
|
||||
@@ -58,8 +58,6 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
return { resolve: value => localPathMediaUrl(protocol, origin, value) }
|
||||
}, [])
|
||||
const last = blocks.length - 1
|
||||
const hasBodyOrToolCall = blocks.some(block => block.kind === 'tool-call'
|
||||
|| (block.kind === 'text' && block.text.trim() !== ''))
|
||||
// Tool-call heads render as tool rows in the chat view's grouping pass, so
|
||||
// a node that is only those heads (or empty) would paint an empty root
|
||||
// between tool groups — skip the shell unless something visible remains.
|
||||
@@ -91,12 +89,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
hidden={reasoningHidden}
|
||||
reveal={revealProcess}
|
||||
>
|
||||
<ReasoningRow
|
||||
text={block.text}
|
||||
running={streaming && i === last}
|
||||
defaultExpanded={!hasBodyOrToolCall && block.text.trim() !== ''}
|
||||
t={t}
|
||||
/>
|
||||
<ReasoningRow text={block.text} running={streaming && i === last} t={t} />
|
||||
</ProcessReasoning>,
|
||||
)
|
||||
break
|
||||
|
||||
@@ -17,23 +17,16 @@ function latestLine(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one assistant reasoning block as the Think disclosure row. The
|
||||
* Render one assistant reasoning block collapsed until the reader opens it. The
|
||||
* collapsed summary omits double-asterisk markers; expanded content preserves
|
||||
* the complete text.
|
||||
* @param props.text - complete or streaming reasoning text.
|
||||
* @param props.running - whether this block is the streaming tail.
|
||||
* @param props.defaultExpanded - expanded state until the reader toggles it.
|
||||
* @param props.t - conversation locale seat for the running status.
|
||||
* @returns the reasoning disclosure.
|
||||
*/
|
||||
export function ReasoningRow({ text, running, defaultExpanded, t }: {
|
||||
text: string
|
||||
running: boolean
|
||||
defaultExpanded: boolean
|
||||
t: ChatViewSlotProps['t']
|
||||
}) {
|
||||
const [expandedOverride, setExpandedOverride] = useState<boolean>()
|
||||
const expanded = expandedOverride ?? defaultExpanded
|
||||
export function ReasoningRow({ text, running, t }: { text: string; running: boolean; t: ChatViewSlotProps['t'] }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const summary = (running ? latestLine(text) : firstLine(text)).replaceAll('**', '')
|
||||
|
||||
return (
|
||||
@@ -54,7 +47,7 @@ export function ReasoningRow({ text, running, defaultExpanded, t }: {
|
||||
open={expanded}
|
||||
expandable
|
||||
expandOnRowClick
|
||||
onToggle={() => { setExpandedOverride(!expanded) }}
|
||||
onToggle={() => { setExpanded(value => !value) }}
|
||||
collapsedContent={(
|
||||
<>
|
||||
<span className={css.separator} aria-hidden />
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
ConversationNodeContext, ConversationNodeDefinition,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { assistantStreamFirstTokenTime } from '@deepseek-ai/dsh-llm/assistant-stream'
|
||||
import type {} from '@deepseek-ai/dsh-llm-retry/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type { AssistantChatData } from '../contract/chat-nodes.ts'
|
||||
@@ -162,17 +161,6 @@ function updateChunk(
|
||||
}
|
||||
}
|
||||
|
||||
/** The Step retains its first token across live chunks and settled retry attempts. */
|
||||
function settleTiming(
|
||||
state: AssistantState,
|
||||
event: SessionEvent<'assistant/message' | 'assistant/attempt'>,
|
||||
): AssistantState {
|
||||
return {
|
||||
...state,
|
||||
firstTokenTime: state.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream),
|
||||
}
|
||||
}
|
||||
|
||||
function settleMessage(
|
||||
state: AssistantState,
|
||||
match: ConversationMatch,
|
||||
@@ -180,7 +168,7 @@ function settleMessage(
|
||||
): AssistantState {
|
||||
const blocks = toAssistantBlocks(event.data.message.content)
|
||||
return {
|
||||
...settleTiming(state, event),
|
||||
...state,
|
||||
blocks,
|
||||
visibleBlocks: countVisibleBlocks(blocks),
|
||||
hidden: false,
|
||||
@@ -248,11 +236,6 @@ function fallbackState(context: ConversationNodeContext<AssistantState>): Assist
|
||||
state = updateChunk(state, match.event.data.chunk, match.event.seq, match.event.time)
|
||||
continue
|
||||
}
|
||||
if (match.event.type === 'assistant/attempt') {
|
||||
state ??= initialState(match.event.data.turn, match.event.data.step)
|
||||
state = settleTiming(state, match.event)
|
||||
continue
|
||||
}
|
||||
if (match.event.type === 'assistant/message') {
|
||||
state ??= initialState(match.event.data.turn, match.event.data.step)
|
||||
state = settleMessage(state, match, match.event)
|
||||
@@ -313,7 +296,6 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
|
||||
match: (event) => {
|
||||
if (event.type === 'step/start') return { id: `${event.data.turn}:${event.data.step}`, role: 'start' }
|
||||
if (event.type === 'assistant/live-chunk'
|
||||
|| event.type === 'assistant/attempt'
|
||||
|| (event.type === 'assistant/message' && event.surfaceOp === 'append')) {
|
||||
return { id: `${event.data.turn}:${event.data.step}`, role: 'update' }
|
||||
}
|
||||
@@ -331,14 +313,13 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
|
||||
return updateChunk(context.state, match.event.data.chunk, match.event.seq, match.event.time)
|
||||
}
|
||||
if (match.event.type === 'assistant/message') return settleMessage(context.state, match, match.event)
|
||||
if (match.event.type === 'assistant/attempt') return settleTiming(context.state, match.event)
|
||||
if (match.event.type === 'llm/retry') {
|
||||
return resetForRetry(context.state)
|
||||
}
|
||||
return context.state
|
||||
},
|
||||
publication: (match) => {
|
||||
if (match.event.type === 'step/start' || match.event.type === 'assistant/attempt') return 'none'
|
||||
if (match.event.type === 'step/start') return 'none'
|
||||
if (match.event.type !== 'assistant/live-chunk') return 'immediate'
|
||||
const type = match.event.data.chunk.type
|
||||
return type === 'usage' || type === 'finish' ? 'none' : 'animation-frame'
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { inspectSystemPrompt } from '../../ui-conversation/src/client/contract/system-prompt.ts'
|
||||
import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm/assistant-stream'
|
||||
import { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { hasAssistantReplyContent } from '../src/client/contract/assistant-content.ts'
|
||||
import { assistantDefinition } from '../src/client/conversation-nodes/assistant.ts'
|
||||
@@ -941,6 +942,56 @@ describe('built-in conversation node Definitions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('omits first-token metrics after live settlement and after reopening the same history', () => {
|
||||
const attemptId = LlmAttemptId('settled-chat-timing')
|
||||
const starts = [
|
||||
at(1, 'turn/start', { turn: 1 }, { time: 1_000 }),
|
||||
at(2, 'step/start', { turn: 1, step: 1 }, { time: 1_010 }),
|
||||
]
|
||||
const value = assembler(starts)
|
||||
const chunk = { type: 'text-delta' as const, index: 0, text: 'Answer' }
|
||||
value.append({
|
||||
type: 'transient',
|
||||
event: {
|
||||
type: 'assistant/live-chunk', seq: 2.5, time: 1_030,
|
||||
data: { attemptId, turn: 1, step: 1, chunk },
|
||||
},
|
||||
})
|
||||
value.flush()
|
||||
expect(node(snapshot(value), 'assistant-step')?.data).toMatchObject({
|
||||
status: 'running', time: 1_030, blocks: [{ kind: 'text', text: 'Answer' }],
|
||||
})
|
||||
|
||||
const stream = new AssistantStreamAccumulator()
|
||||
stream.push({ time: 1_030, chunk })
|
||||
const event = at(3, 'assistant/message', {
|
||||
turn: 1, step: 1, message: assistantMessage('settled-timing', 'Answer'),
|
||||
stream: stream.snapshot(), usage: { outputTokens: 10 },
|
||||
}, { surfaceOp: 'append', time: 1_050 }).event
|
||||
if (event.type !== 'assistant/message') throw new Error('expected Assistant settlement')
|
||||
const settlement = { type: 'event' as const, event }
|
||||
value.settleAssistant(attemptId, settlement)
|
||||
const ends = [
|
||||
at(4, 'step/end', { turn: 1, step: 1 }, { time: 1_060 }),
|
||||
at(5, 'turn/end', { turn: 1, reason: { kind: 'completed' } }, { time: 1_070 }),
|
||||
]
|
||||
for (const end of ends) value.append(end)
|
||||
value.flush()
|
||||
|
||||
const reopened = assembler([...starts, settlement, ...ends])
|
||||
for (const current of [value, reopened]) {
|
||||
const view = snapshot(current)
|
||||
const assistant = (node(view, 'assistant-step')?.data as AssistantChatData).finalNode
|
||||
expect(assistant?.timing).toEqual({
|
||||
stepStartTime: 1_010, firstTokenTime: null, completedTime: 1_050,
|
||||
})
|
||||
const tail = node(view, 'turn-tail')?.data as TurnTailChatData
|
||||
expect(tail.turn).toBe(1)
|
||||
expect(tail.ttftMs).toBeUndefined()
|
||||
expect(tail.tokensPerSecond).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('uses live Assistant deltas without replaying settled embedded streams', () => {
|
||||
const runningHistory = [
|
||||
at(1, 'turn/start', { turn: 1 }),
|
||||
@@ -1056,16 +1107,9 @@ describe('built-in conversation node Definitions', () => {
|
||||
const finalNode = (node(finalizedPacked, 'assistant-step')?.data as AssistantChatData).finalNode
|
||||
expect(finalNode).toMatchObject({
|
||||
blocks: [{ kind: 'text', text: 'done' }],
|
||||
timing: { firstTokenTime: 1_999 },
|
||||
timing: { firstTokenTime: null },
|
||||
})
|
||||
|
||||
const windowed = assembler(finalizedInputs.slice(2), true)
|
||||
const timing = () => (node(snapshot(windowed), 'assistant-step')?.data as AssistantChatData).finalNode?.timing
|
||||
expect(timing()).toMatchObject({ stepStartTime: null, firstTokenTime: 1_999 })
|
||||
windowed.prepend(finalizedInputs.slice(0, 2), false)
|
||||
windowed.flush()
|
||||
expect(timing()).toEqual(finalNode?.timing)
|
||||
|
||||
const namedToolHistory = [
|
||||
at(40, 'turn/start', { turn: 3 }),
|
||||
at(41, 'step/start', { turn: 3, step: 1 }),
|
||||
@@ -1090,7 +1134,7 @@ describe('built-in conversation node Definitions', () => {
|
||||
const namedTool = (node(namedToolPacked, 'assistant-step')?.data as AssistantChatData).finalNode
|
||||
expect(namedTool).toMatchObject({
|
||||
blocks: [{ kind: 'tool-call', callId: 'call-2', name: 'read', argsRaw: '' }],
|
||||
timing: { firstTokenTime: 4_000 },
|
||||
timing: { firstTokenTime: null },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -14,24 +14,27 @@ const t = makeTranslate(zh, commonZh)
|
||||
const renderMessageImages: AssistantMarkdownProps['renderMessageImages'] = () => null
|
||||
|
||||
describe('ReasoningRow', () => {
|
||||
it('collapses when body text arrives and preserves a later manual expansion', () => {
|
||||
it.each([
|
||||
{ kind: 'text' as const, text: 'Answer' },
|
||||
{ kind: 'tool-call' as const, callId: 'call-1', name: 'read', argsRaw: '{}' },
|
||||
])('starts collapsed and preserves manual expansion when $kind arrives', (nextBlock) => {
|
||||
const reasoning = { kind: 'reasoning' as const, text: 'Inspect the session\nCheck persistence' }
|
||||
const view = render(
|
||||
<AssistantMarkdown t={t} blocks={[reasoning]} streaming renderMessageImages={renderMessageImages} />,
|
||||
)
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true')
|
||||
view.rerender(
|
||||
<AssistantMarkdown t={t} blocks={[reasoning, { kind: 'text', text: 'Answer' }]}
|
||||
streaming renderMessageImages={renderMessageImages} />,
|
||||
)
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
view.rerender(
|
||||
<AssistantMarkdown t={t} blocks={[reasoning, { kind: 'text', text: 'Complete answer' }]}
|
||||
streaming={false} renderMessageImages={renderMessageImages} />,
|
||||
<AssistantMarkdown t={t} blocks={[reasoning, nextBlock]} streaming renderMessageImages={renderMessageImages} />,
|
||||
)
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true')
|
||||
view.rerender(
|
||||
<AssistantMarkdown t={t} blocks={[reasoning, nextBlock]} streaming={false} renderMessageImages={renderMessageImages} />,
|
||||
)
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true')
|
||||
expect(view.getByText(/Check persistence/)).toBeTruthy()
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
})
|
||||
|
||||
it('follows the latest streaming line, then restores the settled first line', () => {
|
||||
@@ -44,8 +47,7 @@ describe('ReasoningRow', () => {
|
||||
/>,
|
||||
)
|
||||
expect(view.getByText('运行中')).toBeTruthy()
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
expect(view.getByText('Newest reasoning tokens').parentElement?.getAttribute('data-follow-end'))
|
||||
.toBe('true')
|
||||
|
||||
@@ -69,6 +71,7 @@ describe('ReasoningRow', () => {
|
||||
/>,
|
||||
)
|
||||
const settledSummary = view.getByText('Inspect the session')
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
expect(view.queryByText('运行中')).toBeNull()
|
||||
expect(settledSummary.parentElement?.hasAttribute('data-follow-end')).toBe(false)
|
||||
})
|
||||
@@ -84,7 +87,6 @@ describe('ReasoningRow', () => {
|
||||
)
|
||||
const row = view.getByRole('button')
|
||||
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
fireEvent.click(view.getByText('Inspect the session'))
|
||||
expect(row.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(view.getByText(/Check persistence/)).toBeTruthy()
|
||||
@@ -114,7 +116,6 @@ describe('ReasoningRow', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
expect(view.getByText('Comparing checkout and merge bases')).toBeTruthy()
|
||||
expect(view.queryByText('**Comparing checkout and merge bases**')).toBeNull()
|
||||
|
||||
@@ -122,7 +123,7 @@ describe('ReasoningRow', () => {
|
||||
expect(view.container.querySelector('[class*="thinkBody"]')?.textContent).toBe(text)
|
||||
})
|
||||
|
||||
it('opens reasoning-only replies as plain prose and allows manual collapse', () => {
|
||||
it('expanded Think drops the inline summary and renders plain prose, no IN card', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown
|
||||
t={t}
|
||||
@@ -131,13 +132,10 @@ describe('ReasoningRow', () => {
|
||||
renderMessageImages={renderMessageImages}
|
||||
/>,
|
||||
)
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
expect(view.getAllByText(/Inspect the session/)).toHaveLength(1)
|
||||
expect(view.queryByText('IN')).toBeNull()
|
||||
expect(view.container.querySelector('[class*="ioCard"]')).toBeNull()
|
||||
expect(view.container.querySelector('[class*="thinkBody"]')).not.toBeNull()
|
||||
fireEvent.click(view.getByText('思考'))
|
||||
expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
expect(view.queryByText(/Check persistence/)).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user