mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 15:31:27 +00:00
fix(export): fix TodoWrite tool call display in HTML export
- Restore plan update handling in collect.ts, formatting todo data as markdown checklist - Skip todo_write tool_result processing in normalize.ts to avoid duplicates - Add getMessageTimestamp() and getMessageUuid() methods to maintain message order - Fix version parsing to support @latest tags - Fix Windows spawn EINVAL error (CVE-2024-27980) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
8d8449079d
commit
a364c8212e
8 changed files with 120 additions and 15 deletions
|
|
@ -26,6 +26,7 @@ class ExportSessionContext implements SessionContext {
|
|||
timestamp: number;
|
||||
} | null = null;
|
||||
private activeRecordId: string | null = null;
|
||||
private activeRecordTimestamp: string | null = null;
|
||||
private toolCallMap: Map<string, ExportMessage['toolCall']> = new Map();
|
||||
|
||||
constructor(sessionId: string, config: Config) {
|
||||
|
|
@ -51,14 +52,23 @@ class ExportSessionContext implements SessionContext {
|
|||
case 'tool_call_update':
|
||||
this.handleToolCallUpdate(update);
|
||||
break;
|
||||
case 'plan':
|
||||
this.flushCurrentMessage();
|
||||
this.handlePlanUpdate(update);
|
||||
break;
|
||||
default:
|
||||
// Ignore other update types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setActiveRecordId(recordId: string | null): void {
|
||||
setActiveRecordId(recordId: string | null, timestamp?: string): void {
|
||||
this.activeRecordId = recordId;
|
||||
this.activeRecordTimestamp = timestamp ?? null;
|
||||
}
|
||||
|
||||
private getMessageTimestamp(): string {
|
||||
return this.activeRecordTimestamp ?? new Date().toISOString();
|
||||
}
|
||||
|
||||
private getMessageUuid(): string {
|
||||
|
|
@ -117,7 +127,7 @@ class ExportSessionContext implements SessionContext {
|
|||
this.messages.push({
|
||||
uuid,
|
||||
sessionId: this.sessionId,
|
||||
timestamp: new Date(toolCall.timestamp || Date.now()).toISOString(),
|
||||
timestamp: this.getMessageTimestamp(),
|
||||
type: 'tool_call',
|
||||
toolCall,
|
||||
});
|
||||
|
|
@ -140,6 +150,57 @@ class ExportSessionContext implements SessionContext {
|
|||
}
|
||||
}
|
||||
|
||||
private handlePlanUpdate(update: {
|
||||
entries: Array<{
|
||||
content: string;
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
priority?: string;
|
||||
}>;
|
||||
}): void {
|
||||
// Create a tool_call message for plan updates (TodoWriteTool)
|
||||
// This ensures todos appear at the correct position in the chat
|
||||
const uuid = this.getMessageUuid();
|
||||
const timestamp = this.getMessageTimestamp();
|
||||
|
||||
// Format entries as markdown checklist text for UpdatedPlanToolCall.parsePlanEntries
|
||||
const todoText = update.entries
|
||||
.map((entry) => {
|
||||
const checkbox =
|
||||
entry.status === 'completed'
|
||||
? '[x]'
|
||||
: entry.status === 'in_progress'
|
||||
? '[-]'
|
||||
: '[ ]';
|
||||
return `- ${checkbox} ${entry.content}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const todoContent = [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: {
|
||||
type: 'text',
|
||||
text: todoText,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.messages.push({
|
||||
uuid,
|
||||
sessionId: this.sessionId,
|
||||
timestamp,
|
||||
type: 'tool_call',
|
||||
toolCall: {
|
||||
toolCallId: uuid, // Use the same uuid as toolCallId for plan updates
|
||||
kind: 'todowrite',
|
||||
title: 'TodoWrite',
|
||||
status: 'completed',
|
||||
content: todoContent,
|
||||
timestamp: Date.parse(timestamp),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private flushCurrentMessage(): void {
|
||||
if (!this.currentMessage) return;
|
||||
|
||||
|
|
@ -147,7 +208,7 @@ class ExportSessionContext implements SessionContext {
|
|||
this.messages.push({
|
||||
uuid,
|
||||
sessionId: this.sessionId,
|
||||
timestamp: new Date(this.currentMessage.timestamp).toISOString(),
|
||||
timestamp: this.getMessageTimestamp(),
|
||||
type: this.currentMessage.type,
|
||||
message: {
|
||||
role: this.currentMessage.role,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { Part } from '@google/genai';
|
||||
import { ExitPlanModeTool } from '@qwen-code/qwen-code-core';
|
||||
import { ExitPlanModeTool, ToolNames } from '@qwen-code/qwen-code-core';
|
||||
import type { ChatRecord, Config, Kind } from '@qwen-code/qwen-code-core';
|
||||
import type { ExportMessage, ExportSessionData } from './types.js';
|
||||
|
||||
|
|
@ -103,15 +103,25 @@ function buildToolCallMessageFromResult(
|
|||
config: Config,
|
||||
): ExportMessage | null {
|
||||
const toolCallResult = record.toolCallResult;
|
||||
const toolCallId = toolCallResult?.callId ?? record.uuid;
|
||||
const toolName = extractToolNameFromRecord(record);
|
||||
|
||||
// Skip todo_write tool - it's already handled by plan update in collect.ts
|
||||
// This prevents duplicate todo messages in the export
|
||||
if (toolName === ToolNames.TODO_WRITE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toolCallId = toolCallResult?.callId ?? record.uuid;
|
||||
const functionCallArgs = extractFunctionCallArgs(record);
|
||||
const { kind, title, locations } = resolveToolMetadata(
|
||||
config,
|
||||
toolName,
|
||||
(toolCallResult as { args?: Record<string, unknown> } | undefined)?.args,
|
||||
functionCallArgs ??
|
||||
(toolCallResult as { args?: Record<string, unknown> } | undefined)?.args,
|
||||
);
|
||||
const rawInput = normalizeRawInput(
|
||||
(toolCallResult as { args?: unknown } | undefined)?.args,
|
||||
functionCallArgs ??
|
||||
(toolCallResult as { args?: unknown } | undefined)?.args,
|
||||
);
|
||||
|
||||
const content =
|
||||
|
|
@ -154,6 +164,25 @@ function extractToolNameFromRecord(record: ChatRecord): string {
|
|||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts function call args from a ChatRecord.
|
||||
*/
|
||||
function extractFunctionCallArgs(
|
||||
record: ChatRecord,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!record.message?.parts) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const part of record.message.parts) {
|
||||
if ('functionCall' in part && part.functionCall?.args) {
|
||||
return part.functionCall.args as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves tool metadata (kind, title, locations) from tool registry.
|
||||
*/
|
||||
|
|
@ -197,6 +226,10 @@ function mapToolKind(kind: Kind | undefined, toolName?: string): string {
|
|||
return 'switch_mode';
|
||||
}
|
||||
|
||||
if (toolName && toolName === ToolNames.TODO_WRITE) {
|
||||
return 'todowrite';
|
||||
}
|
||||
|
||||
const allowedKinds = new Set<string>([
|
||||
'read',
|
||||
'edit',
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@
|
|||
*/
|
||||
export function generateExportFilename(extension: string): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
return `export-${timestamp}.${extension}`;
|
||||
return `qwen-code-export-${timestamp}.${extension}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue