mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Fix Assistant terminal idle lifecycle
This commit is contained in:
@@ -268,6 +268,17 @@ runtime cost control, and shared AI transport surfaces.
|
||||
that as an active-turn status strip: no idle diagnostics, but while a turn is
|
||||
loading it must show waiting, current tool/workflow progress, or generating
|
||||
status even when the transcript already contains an in-flight assistant row.
|
||||
The referenced OpenCode source at fetched `origin/dev` commit
|
||||
`09d9cf01f93798939c1284fbe974b6e1f4d2759d` resolves the direct-run wait when
|
||||
`packages/opencode/src/cli/cmd/run/stream.transport.ts` verifies the session
|
||||
is idle, maps `turn.idle` to an idle footer phase in
|
||||
`packages/opencode/src/cli/cmd/run/footer.ts`, and then flushes scrollback
|
||||
separately from later state sync. Pulse's Assistant drawer adapts that
|
||||
completion boundary by clearing the visible active turn as soon as the chat
|
||||
stream has processed its terminal `done` or `error` event; async
|
||||
conversation/session-list refresh may continue afterward, but it must not
|
||||
keep the composer footer or transcript row saying the assistant is still
|
||||
generating.
|
||||
When multiple governed tools are pending, completed tools must not blank the
|
||||
status while another tool is still running, and the status heartbeat should
|
||||
follow the latest progressed pending tool without reordering the transcript's
|
||||
|
||||
@@ -197,6 +197,54 @@ describe('useChat', () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('clears the visible active turn before conversation refresh finishes', async () => {
|
||||
let resolveRefresh!: () => void;
|
||||
let sendResolved = false;
|
||||
mockChat.mockImplementation(
|
||||
(
|
||||
_prompt: string,
|
||||
_session: string | undefined,
|
||||
_model: string | undefined,
|
||||
onEvent: (event: StreamEvent) => void,
|
||||
) => {
|
||||
onEvent({
|
||||
type: 'content',
|
||||
data: { text: 'Pulse currently sees 33 compute resources.' },
|
||||
} as StreamEvent);
|
||||
onEvent({
|
||||
type: 'done',
|
||||
data: { model: 'pulse:local-inventory' },
|
||||
} as StreamEvent);
|
||||
return Promise.resolve();
|
||||
},
|
||||
);
|
||||
const onConversationChanged = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveRefresh = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const { value: chat, dispose } = withRoot(() => useChat({ onConversationChanged }));
|
||||
const sendPromise = chat.sendMessage('how many devices in this').then((result) => {
|
||||
sendResolved = true;
|
||||
return result;
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(chat.isLoading()).toBe(false);
|
||||
expect(chat.messages()[1]).toMatchObject({
|
||||
isStreaming: false,
|
||||
model: 'pulse:local-inventory',
|
||||
});
|
||||
expect(sendResolved).toBe(false);
|
||||
|
||||
resolveRefresh();
|
||||
await expect(sendPromise).resolves.toBe(true);
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('uses the configured default model route for the request and assistant turn', async () => {
|
||||
mockChat.mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -1134,6 +1134,19 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef = abortController;
|
||||
let visibleTurnCompleted = false;
|
||||
|
||||
const completeVisibleTurn = () => {
|
||||
if (requestId !== activeRequestId || visibleTurnCompleted) return;
|
||||
visibleTurnCompleted = true;
|
||||
abortControllerRef = null;
|
||||
setIsLoading(false);
|
||||
if (options?.drainAfter !== false) {
|
||||
queueMicrotask(() => {
|
||||
void drainQueuedFollowUps();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await AIChatAPI.chat(
|
||||
@@ -1155,6 +1168,7 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
if (requestId !== activeRequestId) {
|
||||
return false;
|
||||
}
|
||||
completeVisibleTurn();
|
||||
await notifyConversationChanged();
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -1177,17 +1191,12 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
: msg,
|
||||
),
|
||||
);
|
||||
completeVisibleTurn();
|
||||
await notifyConversationChanged();
|
||||
return false;
|
||||
} finally {
|
||||
if (requestId === activeRequestId) {
|
||||
abortControllerRef = null;
|
||||
setIsLoading(false);
|
||||
if (options?.drainAfter !== false) {
|
||||
queueMicrotask(() => {
|
||||
void drainQueuedFollowUps();
|
||||
});
|
||||
}
|
||||
completeVisibleTurn();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user