Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { useState, type CSSProperties, type ReactNode } from 'react';
import type { ComponentProps } from 'react';
import type { ProjectRecord } from '@maka/core/project';
Expand Down Expand Up @@ -679,6 +680,177 @@ export const InterruptedToolAfterTurnAbort: Story = {
),
};

// Real path: a tool call fails mid-turn and the turn settles as failed. The
// errored tool row renders inside `.maka-turn`, and the turn wears its failed
// Banner (`describeTurnErrorClass('tool_failed')`) with the erroredTool
// execution-state description — the failed-turn chrome no story exercised.
export const FailedTurnWithToolError: Story = {
render: () => (
<ComposedShell
session={{ lastMessageAt: NOW - 4 * 60_000 }}
chat={{
messages: [
user('msg-f-1', 'turn-f', 5, '把 core 里的类型错误修掉,然后跑一遍类型检查确认。'),
{ type: 'turn_state', id: 'state-f-running', turnId: 'turn-f', ts: NOW - 290_000, status: 'running', partialOutputRetained: false },
{ type: 'assistant', id: 'msg-assistant-f', turnId: 'turn-f', ts: NOW - 285_000, text: '先运行类型检查定位问题。', modelId: 'claude-sonnet-4-5' },
{
type: 'tool_call',
id: 'tool-f-1',
turnId: 'turn-f',
ts: NOW - 284_000,
toolName: 'Bash',
activityKind: 'command',
stepId: 'msg-assistant-f',
origin: 'provider',
modelVisibility: 'visible',
args: { command: 'npm run typecheck' },
},
{
type: 'tool_result',
id: 'tool-f-1-result',
turnId: 'turn-f',
ts: NOW - 281_000,
toolUseId: 'tool-f-1',
isError: true,
durationMs: 3_400,
origin: 'provider',
modelVisibility: 'visible',
content: {
kind: 'text',
text: "src/session.ts(88,7): error TS2322: Type 'string' is not assignable to type 'number'.\nnpm run typecheck exited with code 2.",
},
},
{ type: 'turn_state', id: 'state-f-failed', turnId: 'turn-f', ts: NOW - 281_000, status: 'failed', errorClass: 'tool_failed', partialOutputRetained: false },
],
}}
/>
),
play: async ({ canvasElement }) => {
await waitFor(() => {
const banner = canvasElement.querySelector('.maka-turn-failed-banner');
expect(banner?.textContent).toContain('工具调用失败');
expect(banner?.textContent).toContain('这一轮有工具执行出错');
});
},
};

// Real path: the provider rate-limits the request and the turn settles failed.
// The failed Banner carries the rate-limit guidance — the settled provider
// error a bare transcript never shows.
export const ProviderRateLimited: Story = {
render: () => (
<ComposedShell
session={{ lastMessageAt: NOW - 3 * 60_000 }}
chat={{
messages: [
user('msg-r-1', 'turn-r', 4, '再生成三个对照方案,越详细越好。'),
{ type: 'turn_state', id: 'state-r-running', turnId: 'turn-r', ts: NOW - 200_000, status: 'running', partialOutputRetained: false },
{ type: 'turn_state', id: 'state-r-failed', turnId: 'turn-r', ts: NOW - 198_000, status: 'failed', errorClass: 'rate_limit', partialOutputRetained: false },
],
}}
/>
),
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(canvasElement.querySelector('.maka-turn-failed-banner')?.textContent).toContain(
'模型请求太频繁被限流了',
),
);
},
};

// Real path: the provider throttles a live request and Runtime schedules a
// retry. The running turn swaps its working phrase for the retry Banner
// (`ModelProviderRetryIndicator`) — the "retrying" state no story reached.
export const ProviderRetrying: Story = {
render: () => (
<ComposedShell
session={{ status: 'running', streaming: true }}
chat={{
runningStatus: true,
messages: [
user('msg-rr-1', 'turn-rr', 1, '把这份长文档翻译成英文。'),
{ type: 'turn_state', id: 'state-rr', turnId: 'turn-rr', ts: NOW - 20_000, status: 'running', partialOutputRetained: false },
],
liveTurn: {
turnId: 'turn-rr',
phase: 'streamed',
steps: [{ stepId: 'msg-assistant-rr', tools: [] }],
providerRetry: {
event: {
type: 'provider_retry',
phase: 'scheduled',
id: 'retry-rr',
turnId: 'turn-rr',
ts: NOW - 5_000,
attempt: 2,
maxAttempts: 5,
delayMs: 30_000,
remainingMs: 30_000,
reason: 'rate_limit',
},
receivedAtMs: NOW - 5_000,
},
},
}}
/>
),
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(canvasElement.querySelector('.maka-turn-provider-retry')).not.toBeNull(),
);
},
};

// Real path: the app restarted mid-turn, so the last turn is failed with
// errorClass 'app_restarted' and offers safe-resume. The warning-severity
// Banner carries the 继续这一轮 button (`safeResumeAction`) — the recovery
// affordance no story reached.
export const SafeResumeAfterRestart: Story = {
render: () => (
<ComposedShell
session={{ lastMessageAt: NOW - 2 * 60_000 }}
chat={{
safeResumeAction: { pending: false, onResume: noop },
messages: [
user('msg-sr-1', 'turn-sr', 3, '把这份报告整理成要点清单。'),
{ type: 'turn_state', id: 'state-sr-running', turnId: 'turn-sr', ts: NOW - 150_000, status: 'running', partialOutputRetained: false },
{ type: 'assistant', id: 'msg-assistant-sr', turnId: 'turn-sr', ts: NOW - 148_000, text: '好的,我先通读一遍,抓住主要结论——', modelId: 'claude-sonnet-4-5' },
{ type: 'turn_state', id: 'state-sr-failed', turnId: 'turn-sr', ts: NOW - 146_000, status: 'failed', errorClass: 'app_restarted', partialOutputRetained: true },
],
}}
/>
),
play: async ({ canvasElement }) => {
await waitFor(() => {
const banner = canvasElement.querySelector('.maka-turn-failed-banner');
expect(banner?.textContent).toContain('本地应用重启');
expect(banner?.textContent).toContain('继续这一轮');
});
},
};

// Real path: a long session with 120 turns — past the transcript virtualizer's

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This is a useful large-transcript layout/performance fixture, but the virtualization claim is not accurate on this head: ChatView currently renders the full list with turns.map(...); there is no transcript virtualizer/window for 120 turns to cross. Could you describe this as a many-turn/large-transcript state instead, both here and in the PR body? This is a documentation correction and does not block the approval.

// window, so it must stay correct and quiet where a handful of seeded turns
// would never trip the virtualization path.
export const ManyTurns: Story = {
render: () => (
<ComposedShell
session={{ lastMessageAt: NOW - 60_000 }}
chat={{
messages: Array.from({ length: 120 }, (_, index) => {
const turnId = `turn-m-${index}`;
const minutesAgo = (120 - index) * 3;
return [
user(`msg-m-u-${index}`, turnId, minutesAgo, `第 ${index + 1} 轮:这个模块的边界条件该怎么覆盖?`),
assistant(`msg-m-a-${index}`, turnId, minutesAgo - 1, `第 ${index + 1} 轮回答:先列输入域,再对空、超长、并发三类分别加断言。`),
];
}).flat(),
}}
/>
),
};

// Real path: Desktop Computer Use is exposed through the Runtime Host Client
// Capability bridge. The settled observation establishes the confirmed target;
// the following sequence inherits it while live progress replaces the generic
Expand Down