返回 ViMax
events.ts
根目录 / web / src / events.ts
1 import type {AgentEvent, ChatState, Message} from './types';
2
3 export function createChatState(messages: Message[] = []): ChatState {
4 return {messages, busy: false, turnId: '', promptTokens: 0};
5 }
6
7 export function appendLocalUser(state: ChatState, text: string): ChatState {
8 return {
9 ...state,
10 busy: true,
11 messages: [...state.messages, {id: `local-user-${Date.now()}`, role: 'user', text}],
12 };
13 }
14
15 export function composeAgentPrompt(text: string, workspaceUploads: string[] = []) {
16 const message = text.replace(/\s+/g, ' ').trim();
17 const paths = workspaceUploads.map((path) => path.trim()).filter(Boolean);
18 if (paths.length === 0) return message;
19 return `${message} <workspace_uploads>${JSON.stringify(paths)}</workspace_uploads>`;
20 }
21
22 export function applyAgentEvent(state: ChatState, event: AgentEvent): ChatState {
23 const turnId = event.turn_id || state.turnId || `turn-${Date.now()}`;
24 switch (event.type) {
25 case 'turn':
26 return {...state, busy: true, turnId};
27 case 'prompt_trace': {
28 const tokens = event.prompt_trace?.totals?.total_tokens
29 ?? event.prompt_trace?.totals?.total_estimated_tokens
30 ?? event.prompt_trace?.total_estimated_tokens
31 ?? state.promptTokens;
32 return {...state, promptTokens: Math.max(0, Math.round(tokens))};
33 }
34 case 'token':
35 return appendAssistantDelta(state, turnId, event.delta || '');
36 case 'tool_start': {
37 const name = event.tool?.name || event.tool?.requested_name || 'tool';
38 return upsertActivity(state, {
39 id: activityId(turnId, event.tool?.id, name),
40 role: 'activity',
41 tool: name,
42 status: 'running',
43 stage: 'starting',
44 text: 'Starting',
45 });
46 }
47 case 'tool_progress': {
48 const name = event.tool?.name || event.tool?.requested_name || 'tool';
49 return upsertLatestToolActivity(state, name, {
50 role: 'activity',
51 tool: name,
52 status: 'running',
53 stage: event.progress?.stage || 'running',
54 text: event.progress?.message || humanize(event.progress?.stage || 'Running'),
55 });
56 }
57 case 'tool_result': {
58 const name = event.tool_result?.name || 'tool';
59 const ok = event.tool_result?.ok !== false;
60 return upsertLatestToolActivity(state, name, {
61 role: 'activity',
62 tool: name,
63 status: ok ? 'done' : 'error',
64 stage: ok ? 'completed' : 'failed',
65 text: ok ? 'Completed' : cleanError(event.tool_result?.content || 'Tool failed'),
66 });
67 }
68 case 'terminal':
69 if (event.stream !== 'stderr') return state;
70 return {
71 ...state,
72 messages: [...state.messages, {
73 id: `terminal-${Date.now()}`,
74 role: 'activity',
75 status: 'error',
76 tool: 'runtime',
77 text: cleanError(event.line || 'Runtime error'),
78 }],
79 };
80 case 'error':
81 return {
82 ...state,
83 busy: false,
84 messages: [...state.messages, {id: `error-${turnId}-${Date.now()}`, role: 'error', text: event.message || 'Unknown agent error'}],
85 };
86 case 'done': {
87 const hasAssistant = state.messages.some((message) => message.id === `assistant-${turnId}`);
88 const messages = !hasAssistant && event.assistant
89 ? [...state.messages, {id: `assistant-${turnId}`, role: 'assistant' as const, text: event.assistant}]
90 : state.messages;
91 return {...state, messages, busy: false};
92 }
93 case 'bridge_status':
94 if (event.status !== 'error' && event.status !== 'stopped') return state;
95 return {
96 ...state,
97 busy: false,
98 messages: state.messages.map((message) => message.role === 'activity' && message.status === 'running'
99 ? {...message, status: 'error', stage: 'interrupted', text: event.message || 'Generation stopped'}
100 : message),
101 };
102 default:
103 return state;
104 }
105 }
106
107 function appendAssistantDelta(state: ChatState, turnId: string, delta: string): ChatState {
108 if (!delta) return state;
109 const id = `assistant-${turnId}`;
110 const index = state.messages.findIndex((message) => message.id === id);
111 if (index < 0) {
112 return {...state, messages: [...state.messages, {id, role: 'assistant', text: delta}]};
113 }
114 const messages = [...state.messages];
115 messages[index] = {...messages[index], text: `${messages[index].text}${delta}`};
116 return {...state, messages};
117 }
118
119 function upsertActivity(state: ChatState, message: Message): ChatState {
120 const index = state.messages.findIndex((item) => item.id === message.id);
121 if (index < 0) return {...state, messages: [...state.messages, message]};
122 const messages = [...state.messages];
123 messages[index] = {...messages[index], ...message};
124 return {...state, messages};
125 }
126
127 function upsertLatestToolActivity(state: ChatState, tool: string, update: Omit<Message, 'id'>): ChatState {
128 const messages = [...state.messages];
129 for (let index = messages.length - 1; index >= 0; index -= 1) {
130 if (messages[index].role === 'activity' && messages[index].tool === tool && messages[index].status === 'running') {
131 messages[index] = {...messages[index], ...update};
132 return {...state, messages};
133 }
134 }
135 return {...state, messages: [...messages, {id: `activity-${tool}-${Date.now()}`, ...update}]};
136 }
137
138 function activityId(turnId: string, toolId: string | undefined, name: string) {
139 return `activity-${turnId}-${toolId || name}-${Date.now()}`;
140 }
141
142 export function humanize(value: string) {
143 return value
144 .replace(/[_-]+/g, ' ')
145 .replace(/\b\w/g, (letter) => letter.toUpperCase())
146 .replace(/\bVimax\b/g, 'ViMax');
147 }
148
149 function cleanError(value: string) {
150 return value.replace(/\s+/g, ' ').trim();
151 }
152
152 lines TYPESCRIPT