qwen-code/packages/webui/docs/Adapter-README.md
yiliang114 7ca7fec18d docs(webui): update documentation and package references
- Add comprehensive Adapter-README documenting data transformation flow
- Update README.md with correct package name (@qwen-code/webui)
- Add platform adapter guide for Chrome/Web/Share implementations
- Update Storybook configuration and preview styles
- Remove obsolete migration plan and example component

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-01-20 22:59:20 +08:00

306 lines
10 KiB
Markdown

# Data Adapter Layer
This document describes the data transformation flow between different data sources and the webui components.
## Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Data Sources │
├─────────────────────────────────────────────────────────────────────────────┤
│ ACP Protocol (vscode-ide-companion) │ JSONL Files (ChatViewer) │
│ - Real-time streaming │ - Static file format │
│ - Session updates via WebSocket │ - Array of messages │
└─────────────────────────┬───────────────┴──────────────────┬────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Adapter Layer (normalize) │
│ - ACPAdapter: ACP messages → UnifiedMessage │
│ - JSONLAdapter: JSONL format → UnifiedMessage │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Unified Message Format │
│ UnifiedMessage { │
│ id: string │
│ type: 'user' | 'assistant' | 'tool_call' | 'thinking' │
│ timestamp: number │
│ content?: string │
│ toolCall?: ToolCallData │
│ isFirst?: boolean // timeline position │
│ isLast?: boolean // timeline position │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ WebUI Components │
│ - UserMessage │
│ - AssistantMessage │
│ - ThinkingMessage │
│ - ToolCall (Read/Write/Edit/Shell/Search/...) │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Data Structures
### 1. ACP Protocol Format (vscode-ide-companion)
ACP messages come through WebSocket session updates:
```typescript
// Session update types
type AcpSessionUpdate = {
sessionUpdate:
| 'user_message_chunk'
| 'agent_message_chunk'
| 'agent_thought_chunk'
| 'tool_call'
| 'tool_call_update';
content?: { text?: string };
toolCallId?: string;
kind?: string;
title?: string;
status?: string;
rawInput?: unknown;
locations?: Array<{ path: string; line?: number | null }>;
};
```
**Flow:**
1. `qwenSessionUpdateHandler.ts` receives ACP messages
2. Converts to internal format and calls callbacks
3. `WebViewProvider.ts` sends to webview
4. `useToolCalls.ts` manages tool call state
5. `App.tsx` combines into `allMessages` array
### 2. JSONL Format (ChatViewer)
Static JSON array with explicit message types:
```typescript
interface ChatMessageData {
uuid: string;
timestamp: string; // ISO timestamp
type: 'user' | 'assistant' | 'tool_call';
message?: {
role?: string;
parts?: Array<{ text: string }>; // Qwen format
content?: string; // Claude format
};
toolCall?: ToolCallData;
}
```
### 3. ToolCallData (Shared)
```typescript
interface ToolCallData {
toolCallId: string;
kind: string; // 'read' | 'write' | 'edit' | 'bash' | 'grep' | ...
title: string | object;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
rawInput?: string | object;
content?: ToolCallContent[];
locations?: Array<{ path: string; line?: number | null }>;
}
interface ToolCallContent {
type: 'content' | 'diff';
content?: { type: string; text?: string; error?: unknown };
path?: string;
oldText?: string | null;
newText?: string;
}
```
## Adapter Implementation
### ACPAdapter
```typescript
// packages/webui/src/adapters/ACPAdapter.ts
import type { UnifiedMessage, ToolCallData } from './types';
export interface ACPMessage {
type: 'message' | 'in-progress-tool-call' | 'completed-tool-call';
data: unknown;
}
export function adaptACPMessages(messages: ACPMessage[]): UnifiedMessage[] {
return messages.map((item, index, arr) => {
const prev = arr[index - 1];
const next = arr[index + 1];
// Calculate timeline position
const isUserMessage = (m: ACPMessage | undefined) =>
m?.type === 'message' && (m.data as any)?.role === 'user';
const isFirst = !prev || isUserMessage(prev);
const isLast = !next || isUserMessage(next);
switch (item.type) {
case 'message': {
const msg = item.data as {
role: string;
content: string;
timestamp?: number;
};
return {
id: `msg-${index}`,
type:
msg.role === 'user'
? 'user'
: msg.role === 'thinking'
? 'thinking'
: 'assistant',
timestamp: msg.timestamp || Date.now(),
content: msg.content,
isFirst,
isLast,
};
}
case 'in-progress-tool-call':
case 'completed-tool-call': {
const toolCall = item.data as ToolCallData;
return {
id: `tool-${toolCall.toolCallId}`,
type: 'tool_call',
timestamp: Date.now(),
toolCall,
isFirst,
isLast,
};
}
default:
throw new Error(`Unknown message type: ${item.type}`);
}
});
}
```
### JSONLAdapter
```typescript
// packages/webui/src/adapters/JSONLAdapter.ts
import type { UnifiedMessage, ChatMessageData } from './types';
export function adaptJSONLMessages(
messages: ChatMessageData[],
): UnifiedMessage[] {
// Sort by timestamp
const sorted = [...messages].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);
return sorted.map((msg, index, arr) => {
const prev = arr[index - 1];
const next = arr[index + 1];
// User messages break the AI sequence
const isUserType = (m: ChatMessageData | undefined) =>
!m || m.type === 'user';
const isFirst = isUserType(prev);
const isLast = isUserType(next);
// Extract content from different formats
const extractContent = (message?: {
parts?: Array<{ text: string }>;
content?: string;
}) => {
if (!message) return '';
if (message.parts?.length) {
return message.parts.map((p) => p.text).join('');
}
return message.content || '';
};
return {
id: msg.uuid,
type:
msg.type === 'tool_call'
? 'tool_call'
: msg.message?.role === 'thinking'
? 'thinking'
: msg.type,
timestamp: new Date(msg.timestamp).getTime(),
content: extractContent(msg.message),
toolCall: msg.toolCall,
isFirst,
isLast,
};
});
}
```
## Usage
### In ChatViewer (JSONL)
```tsx
import { adaptJSONLMessages } from '../adapters/JSONLAdapter';
const ChatViewer = ({ messages }: { messages: ChatMessageData[] }) => {
const unifiedMessages = useMemo(
() => adaptJSONLMessages(messages),
[messages],
);
return (
<div className="chat-viewer-messages">
{unifiedMessages.map((msg) => renderMessage(msg))}
</div>
);
};
```
### In vscode-ide-companion (ACP)
```tsx
import { adaptACPMessages } from '@qwen-code/webui/adapters';
const App = () => {
const { allMessages } = useWebViewMessages();
const unifiedMessages = useMemo(
() => adaptACPMessages(allMessages),
[allMessages],
);
return (
<div className="chat-messages">
{unifiedMessages.map((msg) => renderMessage(msg))}
</div>
);
};
```
## Timeline Position Calculation
The `isFirst` and `isLast` flags control timeline connector rendering:
- **isFirst=true**: Line starts from bullet point (no line above)
- **isLast=true**: Line ends at bullet point (no line below)
- **Both true**: No timeline connector (single message)
- **Both false**: Full height connector (middle of sequence)
```
User Message (no timeline)
├── Assistant Message [isFirst=true]
│ │ (line starts here)
├── Tool Call
│ │
├── Tool Call
│ │
├── Assistant Message [isLast=true]
│ (line ends here)
User Message (no timeline)
```