codext対応
This commit is contained in:
parent
90cac7e25e
commit
351327a779
@ -35,6 +35,7 @@ export type {
|
|||||||
InitEventData,
|
InitEventData,
|
||||||
ToolUseEventData,
|
ToolUseEventData,
|
||||||
ToolResultEventData,
|
ToolResultEventData,
|
||||||
|
ToolOutputEventData,
|
||||||
TextEventData,
|
TextEventData,
|
||||||
ThinkingEventData,
|
ThinkingEventData,
|
||||||
ResultEventData,
|
ResultEventData,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export type {
|
|||||||
InitEventData,
|
InitEventData,
|
||||||
ToolUseEventData,
|
ToolUseEventData,
|
||||||
ToolResultEventData,
|
ToolResultEventData,
|
||||||
|
ToolOutputEventData,
|
||||||
TextEventData,
|
TextEventData,
|
||||||
ThinkingEventData,
|
ThinkingEventData,
|
||||||
ResultEventData,
|
ResultEventData,
|
||||||
|
|||||||
@ -27,6 +27,11 @@ export interface ToolResultEventData {
|
|||||||
isError: boolean;
|
isError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolOutputEventData {
|
||||||
|
tool: string;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextEventData {
|
export interface TextEventData {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
@ -51,6 +56,7 @@ export type StreamEvent =
|
|||||||
| { type: 'init'; data: InitEventData }
|
| { type: 'init'; data: InitEventData }
|
||||||
| { type: 'tool_use'; data: ToolUseEventData }
|
| { type: 'tool_use'; data: ToolUseEventData }
|
||||||
| { type: 'tool_result'; data: ToolResultEventData }
|
| { type: 'tool_result'; data: ToolResultEventData }
|
||||||
|
| { type: 'tool_output'; data: ToolOutputEventData }
|
||||||
| { type: 'text'; data: TextEventData }
|
| { type: 'text'; data: TextEventData }
|
||||||
| { type: 'thinking'; data: ThinkingEventData }
|
| { type: 'thinking'; data: ThinkingEventData }
|
||||||
| { type: 'result'; data: ResultEventData }
|
| { type: 'result'; data: ResultEventData }
|
||||||
|
|||||||
@ -31,43 +31,6 @@ function extractThreadId(value: unknown): string | undefined {
|
|||||||
return typeof id === 'string' ? id : undefined;
|
return typeof id === 'string' ? id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCodexResult(result: unknown): string {
|
|
||||||
if (result == null) return '';
|
|
||||||
if (typeof result === 'string') return result;
|
|
||||||
if (typeof result !== 'object') return String(result);
|
|
||||||
|
|
||||||
const record = result as Record<string, unknown>;
|
|
||||||
const directFields = ['output_text', 'output', 'content', 'text', 'message'];
|
|
||||||
for (const field of directFields) {
|
|
||||||
const value = record[field];
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(record.output)) {
|
|
||||||
const first = record.output[0];
|
|
||||||
if (typeof first === 'string') return first;
|
|
||||||
if (first && typeof first === 'object') {
|
|
||||||
const text = (first as Record<string, unknown>).text;
|
|
||||||
if (typeof text === 'string') return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(record.choices)) {
|
|
||||||
const firstChoice = record.choices[0] as Record<string, unknown> | undefined;
|
|
||||||
const message = firstChoice?.message as Record<string, unknown> | undefined;
|
|
||||||
const content = message?.content;
|
|
||||||
if (typeof content === 'string') return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.stringify(result, null, 2);
|
|
||||||
} catch {
|
|
||||||
return String(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitInit(
|
function emitInit(
|
||||||
onStream: StreamCallback | undefined,
|
onStream: StreamCallback | undefined,
|
||||||
model: string | undefined,
|
model: string | undefined,
|
||||||
@ -88,6 +51,39 @@ function emitText(onStream: StreamCallback | undefined, text: string): void {
|
|||||||
onStream({ type: 'text', data: { text } });
|
onStream({ type: 'text', data: { text } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitThinking(onStream: StreamCallback | undefined, thinking: string): void {
|
||||||
|
if (!onStream || !thinking) return;
|
||||||
|
onStream({ type: 'thinking', data: { thinking } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitToolUse(
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
tool: string,
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
id: string
|
||||||
|
): void {
|
||||||
|
if (!onStream) return;
|
||||||
|
onStream({ type: 'tool_use', data: { tool, input, id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitToolResult(
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
content: string,
|
||||||
|
isError: boolean
|
||||||
|
): void {
|
||||||
|
if (!onStream) return;
|
||||||
|
onStream({ type: 'tool_result', data: { content, isError } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitToolOutput(
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
tool: string,
|
||||||
|
output: string
|
||||||
|
): void {
|
||||||
|
if (!onStream || !output) return;
|
||||||
|
onStream({ type: 'tool_output', data: { tool, output } });
|
||||||
|
}
|
||||||
|
|
||||||
function emitResult(
|
function emitResult(
|
||||||
onStream: StreamCallback | undefined,
|
onStream: StreamCallback | undefined,
|
||||||
success: boolean,
|
success: boolean,
|
||||||
@ -109,6 +105,237 @@ function determineStatus(content: string, patterns: Record<string, string>): Sta
|
|||||||
return detectStatus(content, patterns);
|
return detectStatus(content, patterns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CodexEvent = {
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodexItem = {
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatFileChangeSummary(changes: Array<{ path?: string; kind?: string }>): string {
|
||||||
|
if (!changes.length) return '';
|
||||||
|
return changes
|
||||||
|
.map((change) => {
|
||||||
|
const kind = change.kind ? `${change.kind}: ` : '';
|
||||||
|
return `${kind}${change.path ?? ''}`.trim();
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCodexItemStart(
|
||||||
|
item: CodexItem,
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
startedItems: Set<string>
|
||||||
|
): void {
|
||||||
|
if (!onStream) return;
|
||||||
|
const id = item.id || `item_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
if (startedItems.has(id)) return;
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'command_execution': {
|
||||||
|
const command = typeof item.command === 'string' ? item.command : '';
|
||||||
|
emitToolUse(onStream, 'Bash', { command }, id);
|
||||||
|
startedItems.add(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mcp_tool_call': {
|
||||||
|
const tool = typeof item.tool === 'string' ? item.tool : 'Tool';
|
||||||
|
const args = (item.arguments ?? {}) as Record<string, unknown>;
|
||||||
|
emitToolUse(onStream, tool, args, id);
|
||||||
|
startedItems.add(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'web_search': {
|
||||||
|
const query = typeof item.query === 'string' ? item.query : '';
|
||||||
|
emitToolUse(onStream, 'WebSearch', { query }, id);
|
||||||
|
startedItems.add(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'file_change': {
|
||||||
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
||||||
|
const summary = formatFileChangeSummary(changes as Array<{ path?: string; kind?: string }>);
|
||||||
|
emitToolUse(onStream, 'Edit', { file_path: summary || 'patch' }, id);
|
||||||
|
startedItems.add(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCodexItemCompleted(
|
||||||
|
item: CodexItem,
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
startedItems: Set<string>,
|
||||||
|
outputOffsets: Map<string, number>,
|
||||||
|
textOffsets: Map<string, number>,
|
||||||
|
thinkingOffsets: Map<string, number>
|
||||||
|
): void {
|
||||||
|
if (!onStream) return;
|
||||||
|
const id = item.id || `item_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'reasoning': {
|
||||||
|
const text = typeof item.text === 'string' ? item.text : '';
|
||||||
|
if (text) {
|
||||||
|
const prev = thinkingOffsets.get(id) ?? 0;
|
||||||
|
if (text.length > prev) {
|
||||||
|
emitThinking(onStream, text.slice(prev) + '\n');
|
||||||
|
thinkingOffsets.set(id, text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'agent_message': {
|
||||||
|
const text = typeof item.text === 'string' ? item.text : '';
|
||||||
|
if (text) {
|
||||||
|
const prev = textOffsets.get(id) ?? 0;
|
||||||
|
if (text.length > prev) {
|
||||||
|
emitText(onStream, text.slice(prev));
|
||||||
|
textOffsets.set(id, text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'command_execution': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
const output = typeof item.aggregated_output === 'string' ? item.aggregated_output : '';
|
||||||
|
if (output) {
|
||||||
|
const prev = outputOffsets.get(id) ?? 0;
|
||||||
|
if (output.length > prev) {
|
||||||
|
emitToolOutput(onStream, 'Bash', output.slice(prev));
|
||||||
|
outputOffsets.set(id, output.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const exitCode = typeof item.exit_code === 'number' ? item.exit_code : undefined;
|
||||||
|
const status = typeof item.status === 'string' ? item.status : '';
|
||||||
|
const isError = status === 'failed' || (exitCode !== undefined && exitCode !== 0);
|
||||||
|
const content = output || (exitCode !== undefined ? `Exit code: ${exitCode}` : '');
|
||||||
|
emitToolResult(onStream, content, isError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mcp_tool_call': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
const status = typeof item.status === 'string' ? item.status : '';
|
||||||
|
const isError = status === 'failed' || !!item.error;
|
||||||
|
const errorMessage =
|
||||||
|
item.error && typeof item.error === 'object' && 'message' in item.error
|
||||||
|
? String((item.error as { message?: unknown }).message ?? '')
|
||||||
|
: '';
|
||||||
|
let content = errorMessage;
|
||||||
|
if (!content && item.result && typeof item.result === 'object') {
|
||||||
|
try {
|
||||||
|
content = JSON.stringify(item.result);
|
||||||
|
} catch {
|
||||||
|
content = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitToolResult(onStream, content, isError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'web_search': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
emitToolResult(onStream, 'Search completed', false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'file_change': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
const status = typeof item.status === 'string' ? item.status : '';
|
||||||
|
const isError = status === 'failed';
|
||||||
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
||||||
|
const summary = formatFileChangeSummary(changes as Array<{ path?: string; kind?: string }>);
|
||||||
|
emitToolResult(onStream, summary || 'Applied patch', isError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCodexItemUpdate(
|
||||||
|
item: CodexItem,
|
||||||
|
onStream: StreamCallback | undefined,
|
||||||
|
startedItems: Set<string>,
|
||||||
|
outputOffsets: Map<string, number>,
|
||||||
|
textOffsets: Map<string, number>,
|
||||||
|
thinkingOffsets: Map<string, number>
|
||||||
|
): void {
|
||||||
|
if (!onStream) return;
|
||||||
|
const id = item.id || `item_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'command_execution': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
const output = typeof item.aggregated_output === 'string' ? item.aggregated_output : '';
|
||||||
|
if (output) {
|
||||||
|
const prev = outputOffsets.get(id) ?? 0;
|
||||||
|
if (output.length > prev) {
|
||||||
|
emitToolOutput(onStream, 'Bash', output.slice(prev));
|
||||||
|
outputOffsets.set(id, output.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'agent_message': {
|
||||||
|
const text = typeof item.text === 'string' ? item.text : '';
|
||||||
|
if (text) {
|
||||||
|
const prev = textOffsets.get(id) ?? 0;
|
||||||
|
if (text.length > prev) {
|
||||||
|
emitText(onStream, text.slice(prev));
|
||||||
|
textOffsets.set(id, text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reasoning': {
|
||||||
|
const text = typeof item.text === 'string' ? item.text : '';
|
||||||
|
if (text) {
|
||||||
|
const prev = thinkingOffsets.get(id) ?? 0;
|
||||||
|
if (text.length > prev) {
|
||||||
|
emitThinking(onStream, text.slice(prev));
|
||||||
|
thinkingOffsets.set(id, text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'file_change': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mcp_tool_call': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'web_search': {
|
||||||
|
if (!startedItems.has(id)) {
|
||||||
|
emitCodexItemStart(item, onStream, startedItems);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call Codex with an agent prompt.
|
* Call Codex with an agent prompt.
|
||||||
*/
|
*/
|
||||||
@ -118,17 +345,19 @@ export async function callCodex(
|
|||||||
options: CodexCallOptions
|
options: CodexCallOptions
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const codex = new Codex();
|
const codex = new Codex();
|
||||||
|
const threadOptions = {
|
||||||
|
model: options.model,
|
||||||
|
workingDirectory: options.cwd,
|
||||||
|
};
|
||||||
const thread = options.sessionId
|
const thread = options.sessionId
|
||||||
? await codex.resumeThread(options.sessionId)
|
? await codex.resumeThread(options.sessionId, threadOptions)
|
||||||
: await codex.startThread();
|
: await codex.startThread(threadOptions);
|
||||||
const threadId = extractThreadId(thread) || options.sessionId;
|
let threadId = extractThreadId(thread) || options.sessionId;
|
||||||
|
|
||||||
const fullPrompt = options.systemPrompt
|
const fullPrompt = options.systemPrompt
|
||||||
? `${options.systemPrompt}\n\n${prompt}`
|
? `${options.systemPrompt}\n\n${prompt}`
|
||||||
: prompt;
|
: prompt;
|
||||||
|
|
||||||
emitInit(options.onStream, options.model, threadId);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.debug('Executing Codex thread', {
|
log.debug('Executing Codex thread', {
|
||||||
agentType,
|
agentType,
|
||||||
@ -136,21 +365,93 @@ export async function callCodex(
|
|||||||
hasSystemPrompt: !!options.systemPrompt,
|
hasSystemPrompt: !!options.systemPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const runOptions = options.model ? { model: options.model } : undefined;
|
const { events } = await thread.runStreamed(fullPrompt);
|
||||||
const result = await (thread as { run: (p: string, o?: unknown) => Promise<unknown> })
|
let content = '';
|
||||||
.run(fullPrompt, runOptions);
|
let success = true;
|
||||||
|
let failureMessage = '';
|
||||||
|
const startedItems = new Set<string>();
|
||||||
|
const outputOffsets = new Map<string, number>();
|
||||||
|
const textOffsets = new Map<string, number>();
|
||||||
|
const thinkingOffsets = new Map<string, number>();
|
||||||
|
|
||||||
const content = normalizeCodexResult(result).trim();
|
for await (const event of events as AsyncGenerator<CodexEvent>) {
|
||||||
emitText(options.onStream, content);
|
if (event.type === 'thread.started') {
|
||||||
emitResult(options.onStream, true, content, threadId);
|
threadId = typeof event.thread_id === 'string' ? event.thread_id : threadId;
|
||||||
|
emitInit(options.onStream, options.model, threadId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'turn.failed') {
|
||||||
|
success = false;
|
||||||
|
if (event.error && typeof event.error === 'object' && 'message' in event.error) {
|
||||||
|
failureMessage = String((event.error as { message?: unknown }).message ?? '');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'error') {
|
||||||
|
success = false;
|
||||||
|
failureMessage = typeof event.message === 'string' ? event.message : 'Unknown error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'item.started') {
|
||||||
|
const item = event.item as CodexItem | undefined;
|
||||||
|
if (item) {
|
||||||
|
emitCodexItemStart(item, options.onStream, startedItems);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'item.updated') {
|
||||||
|
const item = event.item as CodexItem | undefined;
|
||||||
|
if (item) {
|
||||||
|
emitCodexItemUpdate(item, options.onStream, startedItems, outputOffsets, textOffsets, thinkingOffsets);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'item.completed') {
|
||||||
|
const item = event.item as CodexItem | undefined;
|
||||||
|
if (item) {
|
||||||
|
if (item.type === 'agent_message' && typeof item.text === 'string') {
|
||||||
|
content = item.text;
|
||||||
|
}
|
||||||
|
emitCodexItemCompleted(
|
||||||
|
item,
|
||||||
|
options.onStream,
|
||||||
|
startedItems,
|
||||||
|
outputOffsets,
|
||||||
|
textOffsets,
|
||||||
|
thinkingOffsets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
const message = failureMessage || 'Codex execution failed';
|
||||||
|
emitResult(options.onStream, false, message, threadId);
|
||||||
|
return {
|
||||||
|
agent: agentType,
|
||||||
|
status: 'blocked',
|
||||||
|
content: message,
|
||||||
|
timestamp: new Date(),
|
||||||
|
sessionId: threadId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = content.trim();
|
||||||
|
emitResult(options.onStream, true, trimmed, threadId);
|
||||||
|
|
||||||
const patterns = options.statusPatterns || GENERIC_STATUS_PATTERNS;
|
const patterns = options.statusPatterns || GENERIC_STATUS_PATTERNS;
|
||||||
const status = determineStatus(content, patterns);
|
const status = determineStatus(trimmed, patterns);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
agent: agentType,
|
agent: agentType,
|
||||||
status,
|
status,
|
||||||
content,
|
content: trimmed,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
sessionId: threadId,
|
sessionId: threadId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -151,6 +151,9 @@ export function truncate(text: string, maxLength: number): string {
|
|||||||
/** Stream display manager for real-time Claude output */
|
/** Stream display manager for real-time Claude output */
|
||||||
export class StreamDisplay {
|
export class StreamDisplay {
|
||||||
private lastToolUse: string | null = null;
|
private lastToolUse: string | null = null;
|
||||||
|
private currentToolInputPreview: string | null = null;
|
||||||
|
private toolOutputBuffer = '';
|
||||||
|
private toolOutputPrinted = false;
|
||||||
private textBuffer = '';
|
private textBuffer = '';
|
||||||
private thinkingBuffer = '';
|
private thinkingBuffer = '';
|
||||||
private isFirstText = true;
|
private isFirstText = true;
|
||||||
@ -206,6 +209,31 @@ export class StreamDisplay {
|
|||||||
// Start spinner to show tool is executing
|
// Start spinner to show tool is executing
|
||||||
this.startToolSpinner(tool, inputPreview);
|
this.startToolSpinner(tool, inputPreview);
|
||||||
this.lastToolUse = tool;
|
this.lastToolUse = tool;
|
||||||
|
this.currentToolInputPreview = inputPreview;
|
||||||
|
this.toolOutputBuffer = '';
|
||||||
|
this.toolOutputPrinted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display tool output streaming */
|
||||||
|
showToolOutput(output: string, tool?: string): void {
|
||||||
|
if (!output) return;
|
||||||
|
this.stopToolSpinner();
|
||||||
|
this.flushThinking();
|
||||||
|
this.flushText();
|
||||||
|
|
||||||
|
if (tool && !this.lastToolUse) {
|
||||||
|
this.lastToolUse = tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolOutputBuffer += output;
|
||||||
|
const lines = this.toolOutputBuffer.split(/\r?\n/);
|
||||||
|
this.toolOutputBuffer = lines.pop() ?? '';
|
||||||
|
|
||||||
|
this.printToolOutputLines(lines, tool);
|
||||||
|
|
||||||
|
if (this.lastToolUse && this.currentToolInputPreview) {
|
||||||
|
this.startToolSpinner(this.lastToolUse, this.currentToolInputPreview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Display tool result event */
|
/** Display tool result event */
|
||||||
@ -213,6 +241,11 @@ export class StreamDisplay {
|
|||||||
// Stop the spinner first
|
// Stop the spinner first
|
||||||
this.stopToolSpinner();
|
this.stopToolSpinner();
|
||||||
|
|
||||||
|
if (this.toolOutputBuffer) {
|
||||||
|
this.printToolOutputLines([this.toolOutputBuffer], this.lastToolUse ?? undefined);
|
||||||
|
this.toolOutputBuffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
const toolName = this.lastToolUse || 'Tool';
|
const toolName = this.lastToolUse || 'Tool';
|
||||||
if (isError) {
|
if (isError) {
|
||||||
const errorContent = content || 'Unknown error';
|
const errorContent = content || 'Unknown error';
|
||||||
@ -225,6 +258,8 @@ export class StreamDisplay {
|
|||||||
console.log(chalk.green(` ✓ ${toolName}`));
|
console.log(chalk.green(` ✓ ${toolName}`));
|
||||||
}
|
}
|
||||||
this.lastToolUse = null;
|
this.lastToolUse = null;
|
||||||
|
this.currentToolInputPreview = null;
|
||||||
|
this.toolOutputPrinted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Display streaming thinking (Claude's internal reasoning) */
|
/** Display streaming thinking (Claude's internal reasoning) */
|
||||||
@ -308,6 +343,9 @@ export class StreamDisplay {
|
|||||||
reset(): void {
|
reset(): void {
|
||||||
this.stopToolSpinner();
|
this.stopToolSpinner();
|
||||||
this.lastToolUse = null;
|
this.lastToolUse = null;
|
||||||
|
this.currentToolInputPreview = null;
|
||||||
|
this.toolOutputBuffer = '';
|
||||||
|
this.toolOutputPrinted = false;
|
||||||
this.textBuffer = '';
|
this.textBuffer = '';
|
||||||
this.thinkingBuffer = '';
|
this.thinkingBuffer = '';
|
||||||
this.isFirstText = true;
|
this.isFirstText = true;
|
||||||
@ -330,6 +368,9 @@ export class StreamDisplay {
|
|||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
this.showToolResult(event.data.content, event.data.isError);
|
this.showToolResult(event.data.content, event.data.isError);
|
||||||
break;
|
break;
|
||||||
|
case 'tool_output':
|
||||||
|
this.showToolOutput(event.data.output, event.data.tool);
|
||||||
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this.showText(event.data.text);
|
this.showText(event.data.text);
|
||||||
break;
|
break;
|
||||||
@ -372,4 +413,19 @@ export class StreamDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ensureToolOutputHeader(tool?: string): void {
|
||||||
|
if (this.toolOutputPrinted) return;
|
||||||
|
const label = tool || this.lastToolUse || 'Tool';
|
||||||
|
console.log(chalk.gray(` ${chalk.yellow(label)} output:`));
|
||||||
|
this.toolOutputPrinted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private printToolOutputLines(lines: string[], tool?: string): void {
|
||||||
|
if (lines.length === 0) return;
|
||||||
|
this.ensureToolOutputHeader(tool);
|
||||||
|
for (const line of lines) {
|
||||||
|
console.log(chalk.gray(` │ ${line}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user