Merge branch 'codex/20260831-show-progress-messages-a14f9c2d-show-progress-messages-a14f9c2d'
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
# Task: Show assistant progress messages
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20260831-show-progress-messages-a14f9c2d
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20260831-show-progress-messages-a14f9c2d-show-progress-messages-a14f9c2d
|
||||||
|
- Worktree: /Users/inmanx/Documents/makelore-show-progress-messages-a14f9c2d
|
||||||
|
- Base commit: 38f85f6b5e4dc4e2c5e5b9f8f4506554cfd578f5
|
||||||
|
- Owner: codex
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Identify the protocol and UI classification of assistant text emitted before
|
||||||
|
or between tool calls, using the supplied screenshot only as a visual example.
|
||||||
|
- Verify that this stage/progress narration is projected and rendered in the
|
||||||
|
current Makelore Code timeline without exposing Pi-private wire types.
|
||||||
|
- Preserve per-item expand/collapse after a stage explanation has completed,
|
||||||
|
including after the enclosing turn has settled.
|
||||||
|
- Make every collapsed thinking, stage-explanation, and tool-output preview
|
||||||
|
start from its first visible block and line, including horizontally
|
||||||
|
overflowing text.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- Preserve the product-neutral `ConversationMessageNode` / Snapshot / Patch
|
||||||
|
contract and the Main-owned Pi boundary.
|
||||||
|
- Keep private reasoning distinct from user-visible assistant narration and
|
||||||
|
tool execution output.
|
||||||
|
- Do not modify the occupied primary worktree or the separate running
|
||||||
|
development worktree.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Confirmed the raw message is an ordinary Pi assistant message whose turn ends
|
||||||
|
with `stopReason: toolUse`; Main normalizes it to an assistant
|
||||||
|
`ConversationMessageNode` with `stopReason: tool-use`.
|
||||||
|
- Confirmed the Renderer semantically projects non-final assistant text as
|
||||||
|
`assistant-commentary` / “过程说明”, rather than treating it as hidden
|
||||||
|
thinking or as a tool node.
|
||||||
|
- Confirmed active process groups open automatically and show a compact
|
||||||
|
one-line stage explanation with an expand control; settled process groups
|
||||||
|
retain the explanation behind their collapsed process summary while the
|
||||||
|
final assistant answer remains visible.
|
||||||
|
- Added explicit regression coverage proving a completed stage explanation
|
||||||
|
remains collapsed to its first line by default, retains its own expand
|
||||||
|
control, reveals the complete Markdown body, and can be collapsed again.
|
||||||
|
- Changed collapsed process preview selection to use the first displayable
|
||||||
|
block and first non-empty line for thinking, assistant commentary, and tool
|
||||||
|
output. Long lines retain their beginning and use a trailing ellipsis.
|
||||||
|
- Kept every collapsed preview viewport at horizontal offset zero on content
|
||||||
|
and size changes. Tail-only streaming updates no longer replace or replay an
|
||||||
|
unchanged head preview, while expanded content remains complete.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Installed the existing lockfile with pinned pnpm `10.33.4`; 1,015 packages
|
||||||
|
were reused from the local store and no manifest or lockfile changed.
|
||||||
|
- Focused Vitest passed: 3 files / 28 tests covering live Pi event projection,
|
||||||
|
durable session hydration, and Coding timeline process/commentary rendering.
|
||||||
|
- Follow-up focused Vitest passed: 1 file / 9 tests, including completed
|
||||||
|
commentary expand/collapse and stable head-preview assertions across
|
||||||
|
streaming updates.
|
||||||
|
- `pnpm run typecheck`: passed.
|
||||||
|
- Scoped ESLint for the changed Renderer, unit, and Electron E2E files: passed.
|
||||||
|
- `pnpm run build:vite`: passed; only the repository's existing dynamic-import
|
||||||
|
and bundle-size warnings were emitted.
|
||||||
|
- Focused Electron Playwright E2E passed: 1 file / 2 tests, including first-line
|
||||||
|
thinking display, head truncation, and a measured tool-preview
|
||||||
|
`scrollLeft` of zero.
|
||||||
|
- Inspected `electron/coding-runtime/pi/event-projector.ts`,
|
||||||
|
`electron/coding-runtime/pi/session-projector.ts`,
|
||||||
|
`shared/coding-conversation-contracts.ts`, and
|
||||||
|
`src/pages/Chat/CodingConversationTimeline.tsx` across the complete
|
||||||
|
classification path.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- None.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- None recorded.
|
||||||
@@ -72,7 +72,6 @@ const WINDOW_STEP = 100;
|
|||||||
const LONG_OUTPUT_CHARS = 4_000;
|
const LONG_OUTPUT_CHARS = 4_000;
|
||||||
const STREAMING_THINKING_WINDOW_CHARS = 16_000;
|
const STREAMING_THINKING_WINDOW_CHARS = 16_000;
|
||||||
const PROGRESS_PREVIEW_MAX_CHARS = 360;
|
const PROGRESS_PREVIEW_MAX_CHARS = 360;
|
||||||
const PROGRESS_ROLL_CHAR_STEP = 6;
|
|
||||||
|
|
||||||
const NODE_STATUS_LABELS: Record<string, string> = {
|
const NODE_STATUS_LABELS: Record<string, string> = {
|
||||||
declared: '已声明',
|
declared: '已声明',
|
||||||
@@ -652,19 +651,19 @@ function toolActivityIcon(node: ConversationToolNode) {
|
|||||||
return <Hammer className={className} aria-hidden="true" />;
|
return <Hammer className={className} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function latestProgressLine(value: string): string | null {
|
function firstProgressLine(value: string): string | null {
|
||||||
const lines = value
|
const lines = value
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const line = lines
|
const line = lines
|
||||||
.at(-1)
|
.at(0)
|
||||||
?.replace(/\s+/g, ' ')
|
?.replace(/\s+/g, ' ')
|
||||||
.replace(/^#{1,6}\s+/, '')
|
.replace(/^#{1,6}\s+/, '')
|
||||||
.replace(/^(?:[-*+]|\d+[.)])\s+/, '');
|
.replace(/^(?:[-*+]|\d+[.)])\s+/, '');
|
||||||
if (!line) return null;
|
if (!line) return null;
|
||||||
return line.length > PROGRESS_PREVIEW_MAX_CHARS
|
return line.length > PROGRESS_PREVIEW_MAX_CHARS
|
||||||
? `…${line.slice(-(PROGRESS_PREVIEW_MAX_CHARS - 1))}`
|
? `${line.slice(0, PROGRESS_PREVIEW_MAX_CHARS - 1)}…`
|
||||||
: line;
|
: line;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,22 +698,20 @@ const RollingProgressPreview = memo(function RollingProgressPreview({
|
|||||||
codeClassName?: string;
|
codeClassName?: string;
|
||||||
}) {
|
}) {
|
||||||
const viewportRef = useRef<HTMLSpanElement | null>(null);
|
const viewportRef = useRef<HTMLSpanElement | null>(null);
|
||||||
const revision = `${active ? 'active' : 'settled'}:${Math.floor(
|
const revision = `${active ? 'active' : 'settled'}:${text}`;
|
||||||
contentVersion / PROGRESS_ROLL_CHAR_STEP,
|
|
||||||
)}`;
|
|
||||||
const parts = progressInlineParts(text);
|
const parts = progressInlineParts(text);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
viewport.scrollLeft = viewport.scrollWidth;
|
viewport.scrollLeft = 0;
|
||||||
}, [contentVersion, text]);
|
}, [contentVersion, text]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
if (!viewport || typeof ResizeObserver === 'undefined') return;
|
if (!viewport || typeof ResizeObserver === 'undefined') return;
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
viewport.scrollLeft = viewport.scrollWidth;
|
viewport.scrollLeft = 0;
|
||||||
});
|
});
|
||||||
observer.observe(viewport);
|
observer.observe(viewport);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
@@ -729,9 +726,9 @@ const RollingProgressPreview = memo(function RollingProgressPreview({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-progress-alignment="left"
|
data-progress-alignment="left"
|
||||||
|
data-progress-origin="head"
|
||||||
data-progress-update-motion={rollUpdates ? 'roll' : 'none'}
|
data-progress-update-motion={rollUpdates ? 'roll' : 'none'}
|
||||||
data-progress-shimmer={active && shimmer ? 'true' : 'false'}
|
data-progress-shimmer={active && shimmer ? 'true' : 'false'}
|
||||||
data-progress-tail="true"
|
|
||||||
data-roll-revision={rollUpdates ? revision : undefined}
|
data-roll-revision={rollUpdates ? revision : undefined}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
title={text}
|
title={text}
|
||||||
@@ -772,11 +769,11 @@ function contentBlocksProgressPreview(
|
|||||||
blocks: ConversationContentBlock[],
|
blocks: ConversationContentBlock[],
|
||||||
fallback: string,
|
fallback: string,
|
||||||
): string {
|
): string {
|
||||||
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
for (let index = 0; index < blocks.length; index += 1) {
|
||||||
const block = blocks[index];
|
const block = blocks[index];
|
||||||
if (!block) continue;
|
if (!block) continue;
|
||||||
if (block.kind === 'image') return '图片附件';
|
if (block.kind === 'image') return '图片附件';
|
||||||
const preview = latestProgressLine(block.text);
|
const preview = firstProgressLine(block.text);
|
||||||
if (preview) return preview;
|
if (preview) return preview;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -823,14 +820,14 @@ function toolDetailsProgress(details: KnownToolDetails | undefined): string | nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toolProgressPreview(node: ConversationToolNode): string {
|
function toolProgressPreview(node: ConversationToolNode): string {
|
||||||
for (let index = node.output.length - 1; index >= 0; index -= 1) {
|
for (let index = 0; index < node.output.length; index += 1) {
|
||||||
const block = node.output[index];
|
const block = node.output[index];
|
||||||
if (!block) continue;
|
if (!block) continue;
|
||||||
if (block.kind === 'image') return '已生成图片附件';
|
if (block.kind === 'image') return '已生成图片附件';
|
||||||
const outputPreview = latestProgressLine(block.text);
|
const outputPreview = firstProgressLine(block.text);
|
||||||
if (outputPreview) return outputPreview;
|
if (outputPreview) return outputPreview;
|
||||||
}
|
}
|
||||||
return latestProgressLine(node.inputText)
|
return firstProgressLine(node.inputText)
|
||||||
?? toolDetailsProgress(node.details)
|
?? toolDetailsProgress(node.details)
|
||||||
?? statusLabel(node.status);
|
?? statusLabel(node.status);
|
||||||
}
|
}
|
||||||
@@ -1150,7 +1147,7 @@ const ProcessThinking = memo(function ProcessThinking({
|
|||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
rollUpdates={false}
|
rollUpdates={false}
|
||||||
contentVersion={block.text.length}
|
contentVersion={block.text.length}
|
||||||
preview={latestProgressLine(block.text) ?? (
|
preview={firstProgressLine(block.text) ?? (
|
||||||
streaming ? 'Pi 正在形成思路…' : '思考已完成'
|
streaming ? 'Pi 正在形成思路…' : '思考已完成'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ async function installCodingFirstChatHost(
|
|||||||
output: [{
|
output: [{
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
id: 'browser-output-e2e',
|
id: 'browser-output-e2e',
|
||||||
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(5)}Browser ready`,
|
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(8)}Browser ready`,
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
}],
|
}],
|
||||||
details: { schema: 'agent-browser.v1', action: 'status' },
|
details: { schema: 'agent-browser.v1', action: 'status' },
|
||||||
@@ -731,13 +731,13 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|||||||
await expect(activeProcess).toHaveAttribute('open', '');
|
await expect(activeProcess).toHaveAttribute('open', '');
|
||||||
await expect(activeProcess.locator('summary').first()).toContainText('处理中');
|
await expect(activeProcess.locator('summary').first()).toContainText('处理中');
|
||||||
const activeThinking = page.getByLabel('思考过程');
|
const activeThinking = page.getByLabel('思考过程');
|
||||||
await expect(activeThinking).toContainText('Preparing the smallest safe next step.');
|
await expect(activeThinking).toContainText('Inspecting the project before responding.');
|
||||||
await expect(activeThinking).not.toContainText('Inspecting the project before responding.');
|
await expect(activeThinking).not.toContainText('Preparing the smallest safe next step.');
|
||||||
await expect(activeThinking).toHaveAttribute('data-collapsed-lines', '1');
|
await expect(activeThinking).toHaveAttribute('data-collapsed-lines', '1');
|
||||||
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
|
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
|
||||||
await expect(activeThinking).toHaveAttribute('data-streaming', 'true');
|
await expect(activeThinking).toHaveAttribute('data-streaming', 'true');
|
||||||
const activeThinkingPreview = activeThinking.getByTestId('process-progress-preview');
|
const activeThinkingPreview = activeThinking.getByTestId('process-progress-preview');
|
||||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-tail', 'true');
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
||||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
||||||
await expect(activeThinkingPreview).not.toHaveAttribute('data-roll-revision');
|
await expect(activeThinkingPreview).not.toHaveAttribute('data-roll-revision');
|
||||||
@@ -769,6 +769,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|||||||
await expect(activeCommentary).toHaveClass(/text-foreground/);
|
await expect(activeCommentary).toHaveClass(/text-foreground/);
|
||||||
const activeCommentaryPreview = activeCommentary.getByTestId('process-progress-preview');
|
const activeCommentaryPreview = activeCommentary.getByTestId('process-progress-preview');
|
||||||
await expect(activeCommentaryPreview).toContainText('Durable assistant response');
|
await expect(activeCommentaryPreview).toContainText('Durable assistant response');
|
||||||
|
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||||
await expect(activeCommentaryPreview).toHaveClass(/text-foreground/);
|
await expect(activeCommentaryPreview).toHaveClass(/text-foreground/);
|
||||||
await expect(activeCommentaryPreview).not.toHaveClass(/text-muted-foreground/);
|
await expect(activeCommentaryPreview).not.toHaveClass(/text-muted-foreground/);
|
||||||
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
||||||
@@ -787,9 +788,11 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|||||||
const compactToolSummary = compactTool.locator('> summary');
|
const compactToolSummary = compactTool.locator('> summary');
|
||||||
await expect(compactTool).not.toHaveAttribute('open', '');
|
await expect(compactTool).not.toHaveAttribute('open', '');
|
||||||
await expect(compactToolSummary).toHaveAttribute('data-collapsed-lines', '1');
|
await expect(compactToolSummary).toHaveAttribute('data-collapsed-lines', '1');
|
||||||
await expect(compactTool.getByTestId('tool-progress-preview')).toContainText('Browser ready');
|
|
||||||
await expect(compactTool.getByTestId('tool-progress-preview'))
|
await expect(compactTool.getByTestId('tool-progress-preview'))
|
||||||
.toHaveAttribute('data-progress-tail', 'true');
|
.toContainText('Checking browser runtime state');
|
||||||
|
await expect(compactTool.getByTestId('tool-progress-preview')).not.toContainText('Browser ready');
|
||||||
|
await expect(compactTool.getByTestId('tool-progress-preview'))
|
||||||
|
.toHaveAttribute('data-progress-origin', 'head');
|
||||||
const toolProgressViewport = compactTool.getByTestId('tool-progress-preview');
|
const toolProgressViewport = compactTool.getByTestId('tool-progress-preview');
|
||||||
await expect(toolProgressViewport).toHaveAttribute('data-progress-alignment', 'left');
|
await expect(toolProgressViewport).toHaveAttribute('data-progress-alignment', 'left');
|
||||||
await expect(toolProgressViewport).toHaveAttribute('data-progress-shimmer', 'false');
|
await expect(toolProgressViewport).toHaveAttribute('data-progress-shimmer', 'false');
|
||||||
@@ -801,10 +804,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|||||||
scrollWidth: element.scrollWidth,
|
scrollWidth: element.scrollWidth,
|
||||||
}));
|
}));
|
||||||
expect(toolProgressScroll.scrollWidth).toBeGreaterThan(toolProgressScroll.clientWidth);
|
expect(toolProgressScroll.scrollWidth).toBeGreaterThan(toolProgressScroll.clientWidth);
|
||||||
expect(Math.abs(
|
expect(Math.abs(toolProgressScroll.scrollLeft)).toBeLessThanOrEqual(2);
|
||||||
toolProgressScroll.scrollLeft
|
|
||||||
- (toolProgressScroll.scrollWidth - toolProgressScroll.clientWidth),
|
|
||||||
)).toBeLessThanOrEqual(2);
|
|
||||||
const compactToolBounds = await compactToolSummary.boundingBox();
|
const compactToolBounds = await compactToolSummary.boundingBox();
|
||||||
expect(compactToolBounds).not.toBeNull();
|
expect(compactToolBounds).not.toBeNull();
|
||||||
expect(compactToolBounds!.height).toBeLessThanOrEqual(33);
|
expect(compactToolBounds!.height).toBeLessThanOrEqual(33);
|
||||||
|
|||||||
@@ -303,8 +303,8 @@ describe('CodingConversationTimeline', () => {
|
|||||||
expect(within(process).getByText('处理中')).toBeVisible();
|
expect(within(process).getByText('处理中')).toBeVisible();
|
||||||
const thinking = screen.getByLabelText('思考过程');
|
const thinking = screen.getByLabelText('思考过程');
|
||||||
const thinkingPreview = within(thinking).getByTestId('process-progress-preview');
|
const thinkingPreview = within(thinking).getByTestId('process-progress-preview');
|
||||||
expect(thinkingPreview).toHaveTextContent('最后确认本轮只读取、不修改项目内容。');
|
expect(thinkingPreview).toHaveTextContent('先确认 project.json 项目边界,再读取配置。');
|
||||||
expect(thinkingPreview).toHaveAttribute('data-progress-tail', 'true');
|
expect(thinkingPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||||
expect(thinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
expect(thinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
||||||
expect(thinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
expect(thinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
||||||
expect(thinkingPreview).not.toHaveAttribute('data-roll-revision');
|
expect(thinkingPreview).not.toHaveAttribute('data-roll-revision');
|
||||||
@@ -315,7 +315,7 @@ describe('CodingConversationTimeline', () => {
|
|||||||
expect(thinkingPreview).toHaveClass('text-left');
|
expect(thinkingPreview).toHaveClass('text-left');
|
||||||
expect(thinkingPreview.firstElementChild).toHaveClass('inline-flex');
|
expect(thinkingPreview.firstElementChild).toHaveClass('inline-flex');
|
||||||
expect(thinkingPreview.firstElementChild).not.toHaveClass('absolute', 'right-0');
|
expect(thinkingPreview.firstElementChild).not.toHaveClass('absolute', 'right-0');
|
||||||
expect(thinking).not.toHaveTextContent('先确认 project.json 项目边界,再读取配置。');
|
expect(thinking).not.toHaveTextContent('最后确认本轮只读取、不修改项目内容。');
|
||||||
expect(thinking).toHaveAttribute('data-collapsed-lines', '1');
|
expect(thinking).toHaveAttribute('data-collapsed-lines', '1');
|
||||||
expect(thinking).toHaveAttribute('data-expanded', 'false');
|
expect(thinking).toHaveAttribute('data-expanded', 'false');
|
||||||
expect(thinking).toHaveAttribute('data-streaming', 'true');
|
expect(thinking).toHaveAttribute('data-streaming', 'true');
|
||||||
@@ -340,19 +340,19 @@ describe('CodingConversationTimeline', () => {
|
|||||||
expect(processNote).toHaveAttribute('data-collapsed-lines', '1');
|
expect(processNote).toHaveAttribute('data-collapsed-lines', '1');
|
||||||
expect(processNote).toHaveClass('max-h-[1.65em]', 'overflow-hidden');
|
expect(processNote).toHaveClass('max-h-[1.65em]', 'overflow-hidden');
|
||||||
const commentaryPreview = within(processNote).getByTestId('process-progress-preview');
|
const commentaryPreview = within(processNote).getByTestId('process-progress-preview');
|
||||||
expect(commentaryPreview).toHaveTextContent('让我看看 index.html 当前的实际内容:');
|
expect(commentaryPreview).toHaveTextContent('好的,两件事都办:先再试一次产品素材服务,然后整体优化手机移动端体验。');
|
||||||
|
expect(commentaryPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||||
expect(commentaryPreview).toHaveClass('text-foreground');
|
expect(commentaryPreview).toHaveClass('text-foreground');
|
||||||
expect(commentaryPreview).not.toHaveClass('text-muted-foreground/75');
|
expect(commentaryPreview).not.toHaveClass('text-muted-foreground/75');
|
||||||
expect(within(commentaryPreview).getByText('index.html')).toHaveClass('text-foreground');
|
|
||||||
expect(within(commentaryPreview).getByText('index.html')).not.toHaveClass('text-muted-foreground');
|
|
||||||
expect(commentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
expect(commentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
||||||
expect(commentaryPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
expect(commentaryPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
||||||
expect(commentaryPreview).not.toHaveAttribute('data-roll-revision');
|
expect(commentaryPreview).not.toHaveAttribute('data-roll-revision');
|
||||||
expect(commentaryPreview.querySelector('.streaming-progress-shimmer')).not.toBeInTheDocument();
|
expect(commentaryPreview.querySelector('.streaming-progress-shimmer')).not.toBeInTheDocument();
|
||||||
expect(commentaryPreview.querySelector('.streaming-progress-roll')).not.toBeInTheDocument();
|
expect(commentaryPreview.querySelector('.streaming-progress-roll')).not.toBeInTheDocument();
|
||||||
expect(processNote).not.toHaveTextContent('好的,两件事都办');
|
expect(processNote).not.toHaveTextContent('让我看看 index.html 当前的实际内容:');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '展开过程说明' }));
|
fireEvent.click(screen.getByRole('button', { name: '展开过程说明' }));
|
||||||
expect(screen.getByText(/好的,两件事都办/).closest('[data-testid="coding-process-group"]')).toBe(process);
|
expect(screen.getByText(/好的,两件事都办/).closest('[data-testid="coding-process-group"]')).toBe(process);
|
||||||
|
expect(within(processNote).getByText('index.html').tagName).toBe('CODE');
|
||||||
expect(within(processNote).getByTestId('assistant-markdown')).toHaveClass('text-foreground');
|
expect(within(processNote).getByTestId('assistant-markdown')).toHaveClass('text-foreground');
|
||||||
expect(within(processNote).getByTestId('assistant-markdown')).not.toHaveClass('text-muted-foreground');
|
expect(within(processNote).getByTestId('assistant-markdown')).not.toHaveClass('text-muted-foreground');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '收起过程说明' }));
|
fireEvent.click(screen.getByRole('button', { name: '收起过程说明' }));
|
||||||
@@ -362,8 +362,8 @@ describe('CodingConversationTimeline', () => {
|
|||||||
expect(tool.querySelector('summary')).toHaveClass('h-8', 'overflow-hidden');
|
expect(tool.querySelector('summary')).toHaveClass('h-8', 'overflow-hidden');
|
||||||
const toolPreview = within(tool).getByTestId('tool-progress-preview');
|
const toolPreview = within(tool).getByTestId('tool-progress-preview');
|
||||||
const initialToolRevision = toolPreview.getAttribute('data-roll-revision');
|
const initialToolRevision = toolPreview.getAttribute('data-roll-revision');
|
||||||
expect(toolPreview).toHaveTextContent('正在读取入口文件…');
|
expect(toolPreview).toHaveTextContent('已定位项目根目录');
|
||||||
expect(toolPreview).toHaveAttribute('data-progress-tail', 'true');
|
expect(toolPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||||
expect(toolPreview).toHaveAttribute('data-progress-alignment', 'left');
|
expect(toolPreview).toHaveAttribute('data-progress-alignment', 'left');
|
||||||
expect(toolPreview).toHaveAttribute('data-progress-update-motion', 'roll');
|
expect(toolPreview).toHaveAttribute('data-progress-update-motion', 'roll');
|
||||||
expect(toolPreview).toHaveAttribute('data-progress-shimmer', 'true');
|
expect(toolPreview).toHaveAttribute('data-progress-shimmer', 'true');
|
||||||
@@ -387,21 +387,21 @@ describe('CodingConversationTimeline', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
expect(within(thinking).getByTestId('process-progress-preview'))
|
expect(within(thinking).getByTestId('process-progress-preview'))
|
||||||
.toHaveTextContent('正在确认最新入口 main.ts。');
|
.toHaveTextContent('先确认 project.json 项目边界,再读取配置。');
|
||||||
expect(within(thinking).getByTestId('process-progress-preview'))
|
expect(within(thinking).getByTestId('process-progress-preview'))
|
||||||
.not.toHaveAttribute('data-roll-revision');
|
.not.toHaveAttribute('data-roll-revision');
|
||||||
expect(within(thinking).getByTestId('process-progress-preview')
|
expect(within(thinking).getByTestId('process-progress-preview')
|
||||||
.querySelector('.streaming-progress-roll')).not.toBeInTheDocument();
|
.querySelector('.streaming-progress-roll')).not.toBeInTheDocument();
|
||||||
expect(within(processNote).getByTestId('process-progress-preview'))
|
expect(within(processNote).getByTestId('process-progress-preview'))
|
||||||
.toHaveTextContent('正在整理最新结论。');
|
.toHaveTextContent('好的,两件事都办:先再试一次产品素材服务,然后整体优化手机移动端体验。');
|
||||||
expect(within(processNote).getByTestId('process-progress-preview'))
|
expect(within(processNote).getByTestId('process-progress-preview'))
|
||||||
.toHaveClass('text-foreground');
|
.toHaveClass('text-foreground');
|
||||||
expect(within(processNote).getByTestId('process-progress-preview'))
|
expect(within(processNote).getByTestId('process-progress-preview'))
|
||||||
.toHaveAttribute('data-progress-shimmer', 'false');
|
.toHaveAttribute('data-progress-shimmer', 'false');
|
||||||
expect(within(tool).getByTestId('tool-progress-preview'))
|
expect(within(tool).getByTestId('tool-progress-preview'))
|
||||||
.toHaveTextContent('正在追踪最新工具输出:main.ts 已读取。');
|
.toHaveTextContent('已定位项目根目录');
|
||||||
expect(within(tool).getByTestId('tool-progress-preview'))
|
expect(within(tool).getByTestId('tool-progress-preview').getAttribute('data-roll-revision'))
|
||||||
.not.toHaveAttribute('data-roll-revision', initialToolRevision);
|
.toBe(initialToolRevision);
|
||||||
|
|
||||||
fireEvent.click(tool.querySelector('summary')!);
|
fireEvent.click(tool.querySelector('summary')!);
|
||||||
await waitFor(() => expect(tool).toHaveAttribute('open'));
|
await waitFor(() => expect(tool).toHaveAttribute('open'));
|
||||||
@@ -447,7 +447,12 @@ describe('CodingConversationTimeline', () => {
|
|||||||
stopReason: 'tool-use' as const,
|
stopReason: 'tool-use' as const,
|
||||||
blocks: [
|
blocks: [
|
||||||
{ kind: 'thinking' as const, id: 'thinking-completed-process', text: '先读取配置,再核对入口。', status: 'streaming' as const },
|
{ kind: 'thinking' as const, id: 'thinking-completed-process', text: '先读取配置,再核对入口。', status: 'streaming' as const },
|
||||||
{ kind: 'text' as const, id: 'preamble-completed-process', text: '我先检查项目配置。', status: 'complete' as const },
|
{
|
||||||
|
kind: 'text' as const,
|
||||||
|
id: 'preamble-completed-process',
|
||||||
|
text: '我先检查项目配置。\n\n现在开始核对 `index.html` 入口文件。',
|
||||||
|
status: 'complete' as const,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -502,6 +507,29 @@ describe('CodingConversationTimeline', () => {
|
|||||||
.toHaveAttribute('data-progress-shimmer', 'false');
|
.toHaveAttribute('data-progress-shimmer', 'false');
|
||||||
expect(within(settledThinking).getByTestId('process-progress-preview')
|
expect(within(settledThinking).getByTestId('process-progress-preview')
|
||||||
.querySelector('.streaming-progress-shimmer')).not.toBeInTheDocument();
|
.querySelector('.streaming-progress-shimmer')).not.toBeInTheDocument();
|
||||||
|
const completedCommentary = document.querySelector(
|
||||||
|
'[data-node-id="message-work-completed-process"][data-node-kind="assistant-commentary"]',
|
||||||
|
)!;
|
||||||
|
const completedCommentaryViewport = within(completedCommentary as HTMLElement)
|
||||||
|
.getByLabelText('过程说明');
|
||||||
|
expect(completedCommentaryViewport).toHaveAttribute('data-streaming', 'false');
|
||||||
|
expect(completedCommentaryViewport).toHaveAttribute('data-expanded', 'false');
|
||||||
|
expect(within(completedCommentaryViewport).getByTestId('process-progress-preview'))
|
||||||
|
.toHaveTextContent('我先检查项目配置。');
|
||||||
|
expect(within(completedCommentaryViewport).getByTestId('process-progress-preview'))
|
||||||
|
.toHaveAttribute('data-progress-origin', 'head');
|
||||||
|
expect(completedCommentaryViewport).not.toHaveTextContent('现在开始核对 index.html 入口文件。');
|
||||||
|
const completedCommentaryToggle = within(completedCommentary as HTMLElement)
|
||||||
|
.getByRole('button', { name: '展开过程说明' });
|
||||||
|
expect(completedCommentaryToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
fireEvent.click(completedCommentaryToggle);
|
||||||
|
expect(completedCommentaryViewport).toHaveAttribute('data-expanded', 'true');
|
||||||
|
expect(completedCommentaryViewport).toHaveTextContent('我先检查项目配置。');
|
||||||
|
expect(completedCommentaryViewport).toHaveTextContent('现在开始核对 index.html 入口文件。');
|
||||||
|
expect(within(completedCommentaryViewport).getByText('index.html').tagName).toBe('CODE');
|
||||||
|
fireEvent.click(within(completedCommentary as HTMLElement)
|
||||||
|
.getByRole('button', { name: '收起过程说明' }));
|
||||||
|
expect(completedCommentaryViewport).toHaveAttribute('data-expanded', 'false');
|
||||||
const tool = screen.getByText('读取项目配置').closest('details')!;
|
const tool = screen.getByText('读取项目配置').closest('details')!;
|
||||||
expect(tool).not.toHaveAttribute('open');
|
expect(tool).not.toHaveAttribute('open');
|
||||||
fireEvent.click(tool.querySelector('summary')!);
|
fireEvent.click(tool.querySelector('summary')!);
|
||||||
|
|||||||
Reference in New Issue
Block a user