mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
feat(webui): add data adapter layer for ACP and JSONL protocols
- Implement ACPAdapter to convert ACP protocol messages to unified format - Implement JSONLAdapter to convert JSONL format messages to unified format - Define unified message types for consistent component rendering - Add helper functions for timeline position calculation (isFirst/isLast) - Enable cross-platform message format compatibility Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
5ad731fb2b
commit
1861557d15
4 changed files with 345 additions and 0 deletions
109
packages/webui/src/adapters/ACPAdapter.ts
Normal file
109
packages/webui/src/adapters/ACPAdapter.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Adapter for ACP protocol messages (used by vscode-ide-companion)
|
||||
*/
|
||||
|
||||
import type {
|
||||
UnifiedMessage,
|
||||
ACPMessage,
|
||||
ACPMessageData,
|
||||
ToolCallData,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Check if a message is a user message (breaks AI sequence)
|
||||
*/
|
||||
function isUserMessage(msg: ACPMessage | undefined): boolean {
|
||||
if (!msg) return true;
|
||||
if (msg.type !== 'message') return false;
|
||||
const data = msg.data as ACPMessageData;
|
||||
return data?.role === 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt ACP messages to unified format
|
||||
*
|
||||
* @param messages - Array of ACP messages from vscode-ide-companion
|
||||
* @returns Array of unified messages with timeline positions calculated
|
||||
*/
|
||||
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 isFirst = isUserMessage(prev);
|
||||
const isLast = isUserMessage(next);
|
||||
|
||||
switch (item.type) {
|
||||
case 'message': {
|
||||
const msg = item.data as ACPMessageData;
|
||||
return {
|
||||
id: `msg-${index}`,
|
||||
type:
|
||||
msg.role === 'user'
|
||||
? 'user'
|
||||
: msg.role === 'thinking'
|
||||
? 'thinking'
|
||||
: 'assistant',
|
||||
timestamp: msg.timestamp || Date.now(),
|
||||
content: msg.content,
|
||||
fileContext: msg.fileContext,
|
||||
isFirst,
|
||||
isLast,
|
||||
};
|
||||
}
|
||||
|
||||
case 'in-progress-tool-call':
|
||||
case 'completed-tool-call': {
|
||||
const toolCall = item.data as ToolCallData;
|
||||
return {
|
||||
id: `tool-${toolCall.toolCallId}-${item.type}`,
|
||||
type: 'tool_call',
|
||||
timestamp: Date.now(),
|
||||
toolCall,
|
||||
isFirst,
|
||||
isLast,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
// Fallback for unknown types
|
||||
return {
|
||||
id: `unknown-${index}`,
|
||||
type: 'assistant',
|
||||
timestamp: Date.now(),
|
||||
content: '',
|
||||
isFirst,
|
||||
isLast,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if data is a tool call
|
||||
*/
|
||||
export function isToolCallData(data: unknown): data is ToolCallData {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'toolCallId' in data &&
|
||||
'kind' in data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if data is a message
|
||||
*/
|
||||
export function isMessageData(data: unknown): data is ACPMessageData {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'role' in data &&
|
||||
'content' in data
|
||||
);
|
||||
}
|
||||
126
packages/webui/src/adapters/JSONLAdapter.ts
Normal file
126
packages/webui/src/adapters/JSONLAdapter.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Adapter for JSONL format messages (used by ChatViewer)
|
||||
*/
|
||||
|
||||
import type {
|
||||
UnifiedMessage,
|
||||
JSONLMessage,
|
||||
UnifiedMessageType,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Extract text content from different message formats
|
||||
*/
|
||||
function extractContent(message?: {
|
||||
parts?: Array<{ text: string }>;
|
||||
content?: string | unknown[];
|
||||
}): string {
|
||||
if (!message) return '';
|
||||
|
||||
// Qwen format: parts array
|
||||
if (message.parts?.length) {
|
||||
return message.parts.map((p) => p.text).join('');
|
||||
}
|
||||
|
||||
// Claude format: string content
|
||||
if (typeof message.content === 'string') {
|
||||
return message.content;
|
||||
}
|
||||
|
||||
// Claude format: content array
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.filter(
|
||||
(item): item is { type: 'text'; text: string } =>
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'type' in item &&
|
||||
item.type === 'text',
|
||||
)
|
||||
.map((item) => item.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse timestamp string to milliseconds
|
||||
*/
|
||||
function parseTimestamp(timestamp: string): number {
|
||||
const parsed = Date.parse(timestamp);
|
||||
return isNaN(parsed) ? Date.now() : parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the unified message type from JSONL message
|
||||
*/
|
||||
function getMessageType(msg: JSONLMessage): UnifiedMessageType {
|
||||
if (msg.type === 'tool_call') {
|
||||
return 'tool_call';
|
||||
}
|
||||
if (msg.type === 'user') {
|
||||
return 'user';
|
||||
}
|
||||
if (msg.message?.role === 'thinking') {
|
||||
return 'thinking';
|
||||
}
|
||||
return 'assistant';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message is a user type (breaks AI sequence)
|
||||
*/
|
||||
function isUserType(msg: JSONLMessage | undefined): boolean {
|
||||
return !msg || msg.type === 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt JSONL messages to unified format
|
||||
*
|
||||
* @param messages - Array of JSONL messages
|
||||
* @returns Array of unified messages with timeline positions calculated
|
||||
*/
|
||||
export function adaptJSONLMessages(messages: JSONLMessage[]): UnifiedMessage[] {
|
||||
// Sort by timestamp
|
||||
const sorted = [...messages].sort(
|
||||
(a, b) => parseTimestamp(a.timestamp) - parseTimestamp(b.timestamp),
|
||||
);
|
||||
|
||||
return sorted.map((msg, index, arr) => {
|
||||
const prev = arr[index - 1];
|
||||
const next = arr[index + 1];
|
||||
|
||||
// Calculate timeline position
|
||||
const isFirst = isUserType(prev);
|
||||
const isLast = isUserType(next);
|
||||
|
||||
const type = getMessageType(msg);
|
||||
|
||||
return {
|
||||
id: msg.uuid,
|
||||
type,
|
||||
timestamp: parseTimestamp(msg.timestamp),
|
||||
content: type !== 'tool_call' ? extractContent(msg.message) : undefined,
|
||||
toolCall: msg.toolCall,
|
||||
isFirst,
|
||||
isLast,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out empty messages (except tool calls)
|
||||
*/
|
||||
export function filterEmptyMessages(
|
||||
messages: UnifiedMessage[],
|
||||
): UnifiedMessage[] {
|
||||
return messages.filter((msg) => {
|
||||
if (msg.type === 'tool_call') return true;
|
||||
return msg.content && msg.content.trim().length > 0;
|
||||
});
|
||||
}
|
||||
28
packages/webui/src/adapters/index.ts
Normal file
28
packages/webui/src/adapters/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Adapter layer for normalizing different data formats to unified message format
|
||||
*/
|
||||
|
||||
// Type exports
|
||||
export type {
|
||||
UnifiedMessage,
|
||||
UnifiedMessageType,
|
||||
JSONLMessage,
|
||||
ACPMessage,
|
||||
ACPMessageData,
|
||||
ToolCallData,
|
||||
FileContext,
|
||||
} from './types.js';
|
||||
|
||||
// JSONL Adapter (for ChatViewer)
|
||||
export { adaptJSONLMessages, filterEmptyMessages } from './JSONLAdapter.js';
|
||||
|
||||
// ACP Adapter (for vscode-ide-companion)
|
||||
export {
|
||||
adaptACPMessages,
|
||||
isToolCallData,
|
||||
isMessageData,
|
||||
} from './ACPAdapter.js';
|
||||
82
packages/webui/src/adapters/types.ts
Normal file
82
packages/webui/src/adapters/types.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Unified message types for adapter layer
|
||||
*/
|
||||
|
||||
import type { ToolCallData } from '../components/toolcalls/shared/types.js';
|
||||
import type { FileContext } from '../components/messages/UserMessage.js';
|
||||
|
||||
/**
|
||||
* Unified message type used by all webui components
|
||||
*/
|
||||
export type UnifiedMessageType =
|
||||
| 'user'
|
||||
| 'assistant'
|
||||
| 'tool_call'
|
||||
| 'thinking';
|
||||
|
||||
/**
|
||||
* Unified message format - normalized from ACP or JSONL sources
|
||||
*/
|
||||
export interface UnifiedMessage {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Message type */
|
||||
type: UnifiedMessageType;
|
||||
/** Timestamp in milliseconds */
|
||||
timestamp: number;
|
||||
/** Text content (for user/assistant/thinking messages) */
|
||||
content?: string;
|
||||
/** Tool call data (for tool_call type) */
|
||||
toolCall?: ToolCallData;
|
||||
/** Whether this is the first item in an AI response sequence */
|
||||
isFirst: boolean;
|
||||
/** Whether this is the last item in an AI response sequence */
|
||||
isLast: boolean;
|
||||
/** File context for user messages */
|
||||
fileContext?: FileContext[];
|
||||
}
|
||||
|
||||
// Re-export FileContext for convenience
|
||||
export type { FileContext };
|
||||
|
||||
/**
|
||||
* JSONL chat message format (ChatViewer input)
|
||||
*/
|
||||
export interface JSONLMessage {
|
||||
uuid: string;
|
||||
parentUuid?: string | null;
|
||||
sessionId?: string;
|
||||
timestamp: string; // ISO timestamp string
|
||||
type: 'user' | 'assistant' | 'system' | 'tool_call';
|
||||
message?: {
|
||||
role?: string;
|
||||
parts?: Array<{ text: string }>; // Qwen format
|
||||
content?: string | unknown[]; // Claude format
|
||||
};
|
||||
model?: string;
|
||||
toolCall?: ToolCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP message format (vscode-ide-companion input)
|
||||
*/
|
||||
export interface ACPMessage {
|
||||
type: 'message' | 'in-progress-tool-call' | 'completed-tool-call';
|
||||
data: ACPMessageData | ToolCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP text message data
|
||||
*/
|
||||
export interface ACPMessageData {
|
||||
role: 'user' | 'assistant' | 'thinking';
|
||||
content: string;
|
||||
timestamp?: number;
|
||||
fileContext?: FileContext[];
|
||||
}
|
||||
|
||||
export type { ToolCallData };
|
||||
Loading…
Add table
Add a link
Reference in a new issue