Merge pull request #4753 from deepseek-harness/fix/pr-4749-review
fix(chat): address presentation review findings
This commit is contained in:
commit
37535cf44a
19 files changed
+175
-26
No files matched your search
@@ -0,0 +1,2 @@
|
||||
- text: Running
|
||||
- 'button "Think Next paragraph: inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools inspect the loaded context and pending tools"'
|
||||
@@ -0,0 +1,98 @@
|
||||
/** Streaming reasoning previews through the assembled Chat and a paused model adapter. */
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent-default-model'
|
||||
import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, writeComposerDraft } from './support.ts'
|
||||
|
||||
const SUMMARY = `Next paragraph: ${'inspect the loaded context and pending tools '.repeat(8).trim()}`
|
||||
const DELTAS = ['First paragraph', `\nDetails\n\n\n${SUMMARY}`, '\nMore detail']
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./expected/reasoning-preview/running.expected.md', import.meta.url))
|
||||
|
||||
class PausedReasoningAdapter extends LlmAdapter {
|
||||
readonly stages = DELTAS.map(text => ({
|
||||
text,
|
||||
arrived: Promise.withResolvers<undefined>(),
|
||||
proceed: Promise.withResolvers<undefined>(),
|
||||
}))
|
||||
|
||||
release(): void {
|
||||
for (const stage of this.stages) stage.proceed.resolve(undefined)
|
||||
}
|
||||
|
||||
override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
yield { type: 'block-start', index: 0, blockType: 'reasoning' }
|
||||
for (const stage of this.stages) {
|
||||
yield { type: 'reasoning-delta', index: 0, text: stage.text }
|
||||
stage.arrived.resolve(undefined)
|
||||
await stage.proceed.promise
|
||||
options.signal?.throwIfAborted()
|
||||
}
|
||||
yield { type: 'block-end', index: 0, block: { type: 'reasoning', text: DELTAS.join('') } }
|
||||
yield { type: 'block-start', index: 1, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 1, text: 'Done' }
|
||||
yield { type: 'block-end', index: 1, block: { type: 'text', text: 'Done' } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
|
||||
it('shows completed paragraph first lines across blank lines with a right-edge fade', async () => {
|
||||
const scaffold = await launchWebScaffold()
|
||||
const adapter = new PausedReasoningAdapter()
|
||||
try {
|
||||
scaffold.ctx.effect(() => scaffold.ctx.llm.registerAdapter(['reasoning-preview-test'], adapter))
|
||||
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: 'reasoning-preview-test', model: 'paused' })
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
const page = await newEnglishPage(browser)
|
||||
const console = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await page.setViewportSize({ width: 480, height: 1000 })
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
await writeComposerDraft(page, input, 'Show a streamed reasoning preview.')
|
||||
await input.press('Enter')
|
||||
|
||||
const [first, second, third] = adapter.stages
|
||||
if (first === undefined || second === undefined || third === undefined) throw new Error('preview stages are incomplete')
|
||||
await first.arrived.promise
|
||||
const reasoning = page.locator('[data-variant="think"][data-state="running"]')
|
||||
await reasoning.waitFor()
|
||||
expect(await reasoning.getAttribute('data-preview')).toBeNull()
|
||||
|
||||
first.proceed.resolve(undefined)
|
||||
await second.arrived.promise
|
||||
const preview = reasoning.locator('[data-streaming]')
|
||||
await expect.poll(() => preview.textContent()).toBe('First paragraph')
|
||||
expect(await preview.isVisible()).toBe(true)
|
||||
await preview.evaluate((element) => { element.setAttribute('data-retained-preview', 'true') })
|
||||
|
||||
second.proceed.resolve(undefined)
|
||||
await third.arrived.promise
|
||||
await expect.poll(() => preview.textContent()).toBe(SUMMARY)
|
||||
expect(await preview.getAttribute('data-retained-preview')).toBe('true')
|
||||
expect(await preview.evaluate(element => getComputedStyle(element).maskImage)).toContain('linear-gradient')
|
||||
expect(await preview.evaluate(element => element.scrollWidth > element.clientWidth)).toBe(true)
|
||||
expect(await reasoning.getByRole('button').getAttribute('aria-expanded')).toBe('false')
|
||||
await compareOrRefreshGolden(UI_EXPECTED,
|
||||
await captureStableAria(page, '[data-variant="think"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
|
||||
third.proceed.resolve(undefined)
|
||||
await settled
|
||||
await page.getByText('Done', { exact: true }).waitFor()
|
||||
expect(console.pageErrors).toEqual([])
|
||||
expect(console.warnings).toEqual([])
|
||||
} finally {
|
||||
adapter.release()
|
||||
await browser.close()
|
||||
}
|
||||
} finally {
|
||||
adapter.release()
|
||||
await scaffold.close()
|
||||
}
|
||||
})
|
||||
@@ -44,6 +44,7 @@
|
||||
"tests/chat-scroll-fixture.ts",
|
||||
"tests/trajectory-virtualization.e2e.ts",
|
||||
"tests/lifecycle-chrome.e2e.ts",
|
||||
"tests/reasoning-preview.e2e.ts",
|
||||
"tests/details-session-lifecycle.e2e.ts",
|
||||
"tests/sessionless-header.e2e.ts",
|
||||
"tests/document-preview.e2e.ts",
|
||||
|
||||
@@ -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: 3a0e74332a0ee2c5453a024f1ffe9dfbb9ae0163
|
||||
README.zh.md: 89a385a61f08efa86c98d59fd4271f99c3b25210
|
||||
README.md: 2beb4dfadddec0c834c3ef6f4b56826be120baa6
|
||||
README.zh.md: 5d643f4e90b77d2b79ab1bb1c003b399c202e004
|
||||
@@ -8,14 +8,14 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Use this package to render a browser chat from recorded Session conversations, including historical images, localized actions, and restored scroll position. Compact display folds completed-turn process rows while keeping the final answer and independently useful context visible; packed historical Assistant runs remain collapsed. Local transcript and steering submissions appear immediately, remain in their original surface, and disappear atomically when authoritative Session records arrive, while queued submissions stay outside Chat. The package does not assemble or modify model requests.
|
||||
Use this package to render a browser chat from recorded Session conversations, including historical images, localized actions, and restored scroll position. Work-details modes control reasoning previews and fold eligible completed-turn process rows without hiding final answers. Local transcript and steering submissions appear immediately, remain in their original surface, and disappear atomically when authoritative Session records arrive, while queued submissions stay outside Chat. The package does not assemble or modify model requests.
|
||||
|
||||
File-mention providers receive the viewed Session ID with the closing-turn owner, so links into inherited history can address the fork itself.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Reference previews](#reference-previews)
|
||||
- [System prompt row](#system-prompt-row)
|
||||
- [Hidden Chat rows](#system-prompt-row)
|
||||
- [Command and failure rows](#command-and-failure-rows)
|
||||
- [Turn token usage](#turn-token-usage)
|
||||
- [Completed-turn footer](#completed-turn-footer)
|
||||
@@ -35,9 +35,9 @@ Chat supplies file and HTTP(S) navigation through one `MarkdownDelegateProvider`
|
||||
Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: Built-in browser (default) opens a new right-Sidebar Browser tab, while New browser tab opens an external tab. The setting is shown only while the built-in browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on loopback browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references.
|
||||
|
||||
<a id="system-prompt-row"></a>
|
||||
## System prompt row
|
||||
## Hidden Chat rows
|
||||
|
||||
Each nonempty appended `system/message` owns a collapsed prompt row, including a complete prompt at the start of a headerless window; the same-step header does not duplicate it. Chat also shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives.
|
||||
Chat omits system-prompt and `permission` command rows in every work-details mode. The filter changes neither recorded Session events nor Trajectory inspection. Ordinary Context injection and other command rows remain in Chat; Context injection can fold with the owning Turn's process content.
|
||||
|
||||
<a id="command-and-failure-rows"></a>
|
||||
## Command and failure rows
|
||||
@@ -65,11 +65,23 @@ 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 Markdown; incoming answer text, Tool calls, and stream completion preserve that choice. Expanded reasoning uses compact secondary typography: headings add bold weight without changing text size, line height, or color. The collapsed summary stays on one line, follows the latest reasoning line while streaming, and shows the first line after settlement.
|
||||
Each reasoning row starts collapsed, including during streaming and in reasoning-only replies. Clicking the row opens or closes its complete Markdown; incoming answer text, Tool calls, and stream completion preserve that choice. Expanded reasoning uses compact secondary typography: headings add bold weight without changing text size, line height, or color. While streaming, all modes preview the latest paragraph whose first line ends with a newline. Further text in that paragraph does not change the preview; an unfinished single line has no preview. One or more blank lines separate paragraphs. The one-line preview fades at the right edge. After settlement, Compact hides the preview, while Detailed and Expanded show the full text's first line.
|
||||
|
||||
Reasoning rows retain the disclosure's callback, icon, and unchanged content references. Each reasoning row selects its preview visibility from the display policy; mode changes toggle CSS display without unmounting the collapsed summary or updating its parent Assistant renderer. Full Markdown elements are created only while expanded.
|
||||
|
||||
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.
|
||||
Settings → General → Work details stores `ui-chat.transcriptView` as `compact` (default), `detailed`, or `expanded`. A saved legacy `normal` value reads as `detailed` without being written back; it is not offered as an option.
|
||||
|
||||
| Behavior | Compact | Detailed | Expanded |
|
||||
|---|---|---|---|
|
||||
| Settled reasoning preview | Think title only | First line | First line |
|
||||
| Individual reasoning and tool bodies | Manual expansion | Manual expansion | Manual expansion |
|
||||
| Eligible completed-Turn process | Folded by default | Folded by default | Folded by default |
|
||||
|
||||
While a Turn is open, its process rows remain in the transcript; individual reasoning and tool disclosures retain their own expansion state. 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. If that Turn's `turn/start` is loaded, preceding Context injection, reasoning, earlier Assistant material, Tool rows, and Retry rows fold by default. Folding eligibility is per Turn: a complete Turn can fold while Load earlier remains available, and a Turn whose start is missing keeps its process content visible until that start arrives. A closed Turn with no final answer also keeps all process evidence visible.
|
||||
|
||||
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 Context injection contributes no 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, error, max-token, and turn-tail rows stay outside. A newly available process control preserves existing row order, with opening human input before the control and process rows.
|
||||
|
||||
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 process 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. Switching work-details modes preserves manual expansion.
|
||||
|
||||
-----
|
||||
|
||||
@@ -104,6 +116,8 @@ None; Chat presentation does not assemble or mutate provider requests.
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **Secondary process grouping is not implemented** — Detailed and Expanded currently display the same Chat content. Expanded retains whole-Turn folding and does not automatically open individual reasoning or tool bodies.
|
||||
|
||||
- **Developer messages are not displayed** — presentation is intentionally deferred; encountering `developer/message` throws instead of rendering a fallback row.
|
||||
|
||||
- **The transcript reflects the loaded Session window** — older transcript nodes become available only after Session Controller loads the preceding event page. Turn navigation is wider than the window: the rail merges the loaded Turns with the host `turnOutline` projection, so every started Turn gets a fixed-pitch mark (10px apart; a ladder taller than the frame scrolls inside it with gradient fades), and activating an unloaded mark pages history through the Turn's `turn/start` seq before landing on its row. Without the projection (assemblies not mounting `dsh-session-turn-outline`) the rail falls back to loaded Turns only.
|
||||
|
||||
@@ -8,14 +8,14 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
使用本包可在浏览器中渲染已记录的 Session 对话,包括历史图片、本地化操作和滚动位置恢复。紧凑显示会收起已完成轮次的过程行,同时保持最终答案和独立有用的上下文可见;已打包的历史 Assistant 连续消息保持收起。本地 transcript(文本记录)与 steering(中途引导)提交会立即显示并保留在原区域,在权威会话记录到达时原子地消失,而排队中的提交始终不进入 Chat。本包不组装或修改模型请求。
|
||||
使用本包可在浏览器中渲染已记录的 Session 对话,包括历史图片、本地化操作和滚动位置恢复。工作过程展示模式控制思考预览,并收起符合条件的已完成轮次过程行,不隐藏最终答案。本地 transcript(文本记录)与 steering(中途引导)提交会立即显示并保留在原区域,在权威会话记录到达时原子地消失,而排队中的提交始终不进入 Chat。本包不组装或修改模型请求。
|
||||
|
||||
文件提及提供方同时接收当前查看的会话 ID 与收尾轮次的属主信息,因此继承历史中的链接可以指向 fork 自身。
|
||||
|
||||
## 目录
|
||||
|
||||
- [引用预览](#reference-previews)
|
||||
- [系统提示词行](#system-prompt-row)
|
||||
- [Chat 隐藏的行](#system-prompt-row)
|
||||
- [指令与失败行](#command-and-failure-rows)
|
||||
- [轮次 token 用量](#turn-token-usage)
|
||||
- [已完成轮次的页脚](#completed-turn-footer)
|
||||
@@ -35,9 +35,9 @@ Chat 在节点列表外通过一个 `MarkdownDelegateProvider` 提供文件及 H
|
||||
设置 → 通用设置 → 聊天链接打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「内置浏览器」(默认)打开新的右侧 Sidebar Browser tab,「浏览器新标签页」打开外部标签页。该设置项仅在内置浏览器可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。`ui-chat.linkOpening` 偏好在回环地址浏览器中持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。
|
||||
|
||||
<a id="system-prompt-row"></a>
|
||||
## 系统提示词行
|
||||
## Chat 隐藏的行
|
||||
|
||||
每个非空追加的 `system/message` 都拥有一行折叠提示词,包括无 header 窗口起点的完整提示词;同一步骤的 header 不会重复它。Chat 也会为非空的初始请求、显式消息序列起点、文本发生变化的 `system/message` surface 节点替换(文本读取自 `request/header` 处 surface 顺序中最后一个非空存活系统节点),或前序 header 尚未进入已加载历史窗口的非初始请求显示一行默认折叠的 `系统提示词`。即使系统文本未变,恢复也会重复该行,包括分页补齐前序 header 和系统节点后;同一序列内仅配置或仅工具变化、工具步骤与重试不会重复,且 `system/message` 事件绝不会渲染为对话消息。该行位于请求的用户消息之前,与提供方 envelope 顺序一致;展开后显示模型所见的确切文本,并保留其原始换行。系统节点为空或位于已加载窗口之外的请求不创建该行,直到包含该节点的分页到达。
|
||||
Chat 在所有工作过程展示模式下都不显示系统提示词行和 `permission` 命令行。该过滤不改变已记录的 Session 事件或 Trajectory 查看能力。普通上下文注入和其他命令行仍保留在 Chat 中;上下文注入可随所属轮次的过程内容一起收起。
|
||||
|
||||
<a id="command-and-failure-rows"></a>
|
||||
## 指令与失败行
|
||||
@@ -65,11 +65,23 @@ Chat 在节点列表外通过一个 `MarkdownDelegateProvider` 提供文件及 H
|
||||
<a id="turn-process-folding"></a>
|
||||
## 轮次过程折叠
|
||||
|
||||
每个思考行初始都折叠,包括流式输出期间和仅含思考的回复。点击该行可展开或收起完整 Markdown;后续回答文本、工具调用和流结束都保留用户的选择。展开的思考采用紧凑的次级排版:标题只加粗,不改变字号、行高或颜色。折叠摘要保持单行,在流式输出期间跟随最新思考行,结算后显示第一行。
|
||||
每个思考行初始都折叠,包括流式输出期间和仅含思考的回复。点击该行可展开或收起完整 Markdown;后续回答文本、工具调用和流结束都保留用户的选择。展开的思考采用紧凑的次级排版:标题只加粗,不改变字号、行高或颜色。流式输出期间,三档都预览最近一个首行以换行符结束的段落;该段落后续文本不会改变预览,未写完的单行不显示预览。一个或多个空行分隔段落。预览保持单行,右侧渐隐。结算后,「简洁」隐藏预览,「详细」和「完全展开」显示全文首行。
|
||||
|
||||
思考行保留折叠控件的回调、图标和未变化内容的引用。每个思考行从展示策略中选择预览可见性;模式切换只改变 CSS 显示状态,不卸载折叠摘要,也不更新其父级 Assistant 渲染器。完整 Markdown 元素仅在展开时创建。
|
||||
|
||||
「设置 → 通用设置」提供持久化到 `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 默认收起。
|
||||
「设置 → 通用设置 → 工作过程展示」将 `ui-chat.transcriptView` 保存为 `compact`(默认)、`detailed` 或 `expanded`。已保存的旧值 `normal` 读取为 `detailed`,不自动回写,也不再作为选项提供。
|
||||
|
||||
| 行为 | 简洁 | 详细 | 完全展开 |
|
||||
|---|---|---|---|
|
||||
| 已完成思考的预览 | 只显示「思考」标题 | 首行 | 首行 |
|
||||
| 单个思考与工具正文 | 手动展开 | 手动展开 | 手动展开 |
|
||||
| 符合条件的已完成轮次过程 | 默认收起 | 默认收起 | 默认收起 |
|
||||
|
||||
轮次打开期间,过程行保留在 transcript 中,单个思考与工具控件各自保留开合状态。到 `turn/end` 时,最后一个步骤只有在包含非空文本、图片或未知可见块且不含工具调用块时才成为最终答案边界。若该轮次的 `turn/start` 已加载,边界之前的上下文注入、思考、较早 Assistant 内容、工具行与重试行默认收起。折叠资格逐轮判断:即使仍可「加载更早」,完整轮次也能收起;缺少起点的轮次则保持过程内容可见,直到该起点加载。关闭时没有最终答案的轮次也保留全部过程证据。
|
||||
|
||||
控件展示覆盖整个轮次的非 subagent 工具调用数、最终答案之前带回复内容的 Assistant 消息数和 subagent 委派数。值为 0 的分段省略;工具调用与 subagent 两项互斥,上下文注入不增加计数。三项全为 0 时过程仍会收起,控件标题显示「已思考」(英文为 `Thought for a while`)。摘要下方的通栏分隔线将其与正文或展开后的过程行隔开。用户与 steering 消息、错误、最大 token 与 turn-tail 行留在过程之外。新的过程控件保留既有行的相对顺序,开场人工输入位于控件和过程行之前。
|
||||
|
||||
稳定 Chat Node Seat 会让每个 renderer 保持挂载,隐藏成员不产生消息流间距;只有中间没有独立输入时,收起控件才与正文相隔 8px。完成后的收起不依赖是否跟随尾部,因此正在上方阅读的用户可能看到 transcript 重排。若自动收起会隐藏当前键盘焦点,则过程保持展开且焦点留在原处;手动收起会先把焦点移到过程控件,再隐藏成员。会话作用域存储只记录用户手动展开的「轮次 + 正文步骤」generation;不同正文 generation 默认收起。切换工作过程展示模式会保留手动展开状态。
|
||||
|
||||
-----
|
||||
|
||||
@@ -104,6 +116,8 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **二级过程分组尚未实现**——「详细」与「完全展开」目前展示相同的 Chat 内容。「完全展开」仍保留整轮折叠,不会自动打开单个思考或工具正文。
|
||||
|
||||
- **不展示 developer 消息** — 展示能力有意留待后续实现;遇到 `developer/message` 时抛出错误,不渲染回退行。
|
||||
|
||||
- **transcript 只反映已加载的 Session 窗口**——只有会话控制器加载前一页事件后,更早的 transcript node 才会出现。轮次导航比窗口更宽:轨道把已加载的轮次与宿主 `turnOutline` 投影合并,每个已开始的轮次都有固定间距刻度(相隔 10px;阶梯高于外框时在框内滚动并以渐变淡出标示可滚方向),激活未加载刻度会先把历史分页拉到该轮次的 `turn/start` seq 再落到它的行上。没有该投影时(未挂载 `dsh-session-turn-outline` 的装配),轨道回退到仅显示已加载轮次。
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface AssistantMarkdownProps {
|
||||
interrupted?: boolean | undefined
|
||||
/** Render consecutive image blocks through the attachment slot. */
|
||||
renderMessageImages: ChatNodeOwnerProps['renderMessageImages']
|
||||
/** Hide reasoning that belongs to the Turn-level process disclosure or a collapsed step group. */
|
||||
/** Hide reasoning that belongs to the Turn-level process disclosure. */
|
||||
reasoningHidden?: boolean | undefined
|
||||
/** Live display policy for reasoning summaries. */
|
||||
usePresentation: UsePresentation
|
||||
|
||||
@@ -16,7 +16,7 @@ function firstLine(text: string): string {
|
||||
function latestCompletedParagraphFirstLine(text: string): string {
|
||||
let summary = ''
|
||||
let paragraphStart = 0
|
||||
const separator = /\r?\n[\t ]*\r?\n/g
|
||||
const separator = /\r?\n(?:[\t ]*\r?\n)+/g
|
||||
while (true) {
|
||||
const nextParagraph = separator.exec(text)
|
||||
const paragraphEnd = nextParagraph === null ? text.length
|
||||
|
||||
@@ -31,7 +31,6 @@ export type { ChatStoreState, ToolCallId, TurnProcessViewEntry } from './contrac
|
||||
export type { TranscriptViewRowInjected, TranscriptViewRowProps } from './settings/TranscriptViewRow.tsx'
|
||||
export type { TranscriptViewMode } from '../chat-settings.ts'
|
||||
export type { ChatPresentationPolicy } from './presentation-policy.ts'
|
||||
export { derivePresentationPolicy, presentationPolicyFor } from './presentation-policy.ts'
|
||||
export type {
|
||||
AssistantActionOwnerProps, ChatFileMentions, ChatNodeOwnerProps, ChatNodeTurnDataInjected,
|
||||
ChatNodeViewProps, ChatScrollPosition, ChatStore, ChatViewInjected, ChatViewSlotProps,
|
||||
|
||||
@@ -13,11 +13,7 @@ export interface ChatPresentationPolicy {
|
||||
readonly mode: TranscriptViewMode
|
||||
/** Whether a normally completed Turn folds its process rows behind the whole-Turn control. */
|
||||
readonly foldCompletedTurns: boolean
|
||||
/**
|
||||
* How consecutive process rows between Assistant replies present: `collapsed`
|
||||
* groups them behind a step-group row that starts closed; `none` renders
|
||||
* every row directly under the whole-Turn control with no group rows.
|
||||
*/
|
||||
/** Reserved secondary-grouping preference; no renderer consumes this field yet. */
|
||||
readonly stepGrouping: 'collapsed' | 'none'
|
||||
/** Whether a settled reasoning row previews its first line beside the Think title. */
|
||||
readonly settledReasoningPreview: boolean
|
||||
|
||||
@@ -88,6 +88,12 @@ function storeOf(runtime: SlotTestRuntime, key: 'conversation.session' | 'conver
|
||||
}
|
||||
|
||||
describe('Chat apply wiring', () => {
|
||||
it('keeps presentation-policy helpers out of the public browser entry', async () => {
|
||||
const entry = await import('../src/client/index.ts')
|
||||
expect(entry).not.toHaveProperty('derivePresentationPolicy')
|
||||
expect(entry).not.toHaveProperty('presentationPolicyFor')
|
||||
})
|
||||
|
||||
it('contributes Chat View, node renderers, and stats', async () => {
|
||||
const b = await bench()
|
||||
const views = b.runtime.slots.entries('conversation.view')
|
||||
|
||||
@@ -26,6 +26,12 @@ describe('ReasoningRow', () => {
|
||||
['First\nSecond\nThird\n', 'First'],
|
||||
['First\n\nNext unfinished', 'First'],
|
||||
['First\n\nNext complete\nDetails', 'Next complete'],
|
||||
['First\n\n\nNext complete\n', 'Next complete'],
|
||||
['First\n \n\t\nNext complete\nDetails', 'Next complete'],
|
||||
['First\r\n\r\n\r\nNext complete\r\n', 'Next complete'],
|
||||
['First\r\n \r\n\t\r\nNext complete\r\nDetails', 'Next complete'],
|
||||
['First\n\n\nNext unfinished', 'First'],
|
||||
['\n\n\nFirst complete\n', 'First complete'],
|
||||
['First\r\n \t\r\nNext complete\r\n', 'Next complete'],
|
||||
['First\n\n\n', 'First'],
|
||||
])('previews the completed paragraph first line for %j', (text, summary) => {
|
||||
|
||||
@@ -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-tool/README.md
|
||||
README.md: 6a63c14d163742b1818320925248405831bd87b6
|
||||
README.zh.md: e53a97404ae17f0e93242c11dcc0b1f066542c12
|
||||
README.md: 3e216d9670f4fa58b0590f8cede54927d947d56d
|
||||
README.zh.md: 6445ee9d62555de36901300809955b7cb227f66f
|
||||
@@ -27,6 +27,8 @@ English | [中文](README.zh.md)
|
||||
|
||||
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks.
|
||||
|
||||
Shared Tool rows and Bash rows retain error and warning colors for failed and stopped summaries, including on hover. Hover darkens only summaries without those states.
|
||||
|
||||
### Registering a business tool view
|
||||
|
||||
An owning business package registers its wire Tool name into `tool.call.toolview`:
|
||||
|
||||
@@ -27,6 +27,8 @@ kind: "package-reference"
|
||||
|
||||
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。
|
||||
|
||||
共享工具行和 Bash 行的失败、停止摘要在悬停时仍保留错误色和警告色;只有不处于这两种状态的摘要会在悬停时加深。
|
||||
|
||||
### 注册业务工具视图
|
||||
|
||||
拥有该视图的业务包将其 wire 工具名称注册进 `tool.call.toolview`:
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
|
||||
.row:hover .title,
|
||||
.row:hover .summary:not(.errorSummary),
|
||||
.row:hover .summary:not(.errorSummary):not(.stoppedSummary),
|
||||
.row:hover .summarySuffix {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
}
|
||||
|
||||
.root:hover .title,
|
||||
.root:hover .summary:not(.errorSummary) {
|
||||
.root:hover .summary:not(.errorSummary):not(.stoppedSummary) {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ function declarations(selector: string): string[] {
|
||||
}
|
||||
|
||||
describe('ToolRow.module.css summary line', () => {
|
||||
it.each([
|
||||
['ToolRow', '../src/client/tool/components/ToolRow.module.css'],
|
||||
['BashRow', '../src/client/tool/toolviews/bash-sample.module.css'],
|
||||
] as const)('%s keeps stopped and error summaries outside hover darkening', (_name, path) => {
|
||||
const sheet = readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
||||
expect(sheet).toContain('.summary:not(.errorSummary):not(.stoppedSummary)')
|
||||
})
|
||||
|
||||
it('keeps the summary suffix on one line and unshrunk', () => {
|
||||
// `flex: none` stops the box shrinking, not the text wrapping: without
|
||||
// `nowrap`, a row too narrow for title + separator + suffix wraps the `+n`
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"apps/web/tests/chat-scroll-fixture.ts",
|
||||
"apps/web/tests/trajectory-virtualization.e2e.ts",
|
||||
"apps/web/tests/lifecycle-chrome.e2e.ts",
|
||||
"apps/web/tests/reasoning-preview.e2e.ts",
|
||||
"apps/web/tests/details-session-lifecycle.e2e.ts",
|
||||
"apps/web/tests/sessionless-header.e2e.ts",
|
||||
"apps/web/tests/document-preview.e2e.ts",
|
||||
|
||||
Reference in new issue
Block a user