Phase 1 run-control semantics and run_state events
This commit is contained in:
@@ -715,6 +715,10 @@ async function sendMessage(client, overrideText) {
|
||||
scrollToBottom();
|
||||
|
||||
// Create placeholder for assistant response
|
||||
const statusLine = document.createElement('div');
|
||||
statusLine.className = 'px-1 text-[11px] leading-none text-zinc-500 select-none hidden';
|
||||
statusLine.textContent = 'Run status: queued';
|
||||
_elements.messages.appendChild(statusLine);
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50 streaming-cursor';
|
||||
placeholder.innerHTML = '<span class="text-zinc-500">Thinking...</span>';
|
||||
@@ -759,6 +763,22 @@ async function sendMessage(client, overrideText) {
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
stream.on('run_state', (data) => {
|
||||
if (!data || !data.state) {
|
||||
return;
|
||||
}
|
||||
const labels = {
|
||||
start: 'Run status: working',
|
||||
cancel_requested: 'Run status: cancellation requested',
|
||||
cancelled: 'Run status: cancelled',
|
||||
complete: 'Run status: complete',
|
||||
error: `Run status: error${data.message ? ` (${data.message})` : ''}`,
|
||||
};
|
||||
statusLine.textContent = labels[data.state] || `Run status: ${data.state}`;
|
||||
statusLine.classList.remove('hidden');
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
const done = await stream.result;
|
||||
const content = done?.content ?? done?.text ?? '(no response)';
|
||||
const assistantMessage = createMessageEl('assistant', content, Date.now());
|
||||
@@ -771,6 +791,7 @@ async function sendMessage(client, overrideText) {
|
||||
_cancelling = false;
|
||||
updateSendButton();
|
||||
clearPendingAttachments();
|
||||
statusLine.remove();
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,4 +185,55 @@ describe('ChatPage wiring', () => {
|
||||
|
||||
expect(calls.some((entry) => entry.method === 'agent.cancel')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders run_state updates during streaming', async () => {
|
||||
let resolveResult: ((value: { content: string }) => void) | undefined;
|
||||
const handlers = new Map<string, Array<(data: any) => void>>();
|
||||
const stream = {
|
||||
on(event: string, cb: (data: any) => void) {
|
||||
if (!handlers.has(event)) {
|
||||
handlers.set(event, []);
|
||||
}
|
||||
handlers.get(event)?.push(cb);
|
||||
},
|
||||
emit(event: string, data: any) {
|
||||
for (const cb of handlers.get(event) ?? []) {
|
||||
cb(data);
|
||||
}
|
||||
},
|
||||
result: new Promise<{ content: string }>((resolve) => {
|
||||
resolveResult = resolve;
|
||||
}),
|
||||
};
|
||||
|
||||
const client = {
|
||||
async call(method: string) {
|
||||
if (method === 'sessions.list') {
|
||||
return { sessions: [] };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
stream() {
|
||||
return stream;
|
||||
},
|
||||
};
|
||||
|
||||
await ChatPage.render(root, client);
|
||||
|
||||
const input = root.querySelector('#chat-input');
|
||||
input.value = 'hello';
|
||||
root.querySelector('#chat-send').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
|
||||
stream.emit('run_state', { state: 'start' });
|
||||
await Promise.resolve();
|
||||
|
||||
const statusLine = Array.from(root.querySelectorAll('div'))
|
||||
.find((el: any) => String(el.textContent ?? '').startsWith('Run status:'));
|
||||
expect(statusLine).toBeTruthy();
|
||||
expect(statusLine.classList.contains('hidden')).toBe(false);
|
||||
|
||||
resolveResult?.({ content: 'ok' });
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user