mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 03:04:03 +00:00
Let Assistant send during provider checks
This commit is contained in:
@@ -77,10 +77,11 @@ runtime cost control, and shared AI transport surfaces.
|
||||
by `internal/ai/`, not Patrol runtime-finding wording, and the drawer may
|
||||
surface the result as actionable retry/settings status plus same-model
|
||||
configured-provider alternatives without converting it into
|
||||
assistant-authored output. The composer must keep typed text and focus while
|
||||
the selected provider is checking or failed, but it must not dispatch a user
|
||||
turn until that selected provider route is ready or the operator chooses a
|
||||
ready alternative.
|
||||
assistant-authored output. Provider checking is a background diagnostic and
|
||||
must not delay the user's first useful chat turn; a confirmed selected-route
|
||||
provider error must keep typed text and focus while blocking dispatch until
|
||||
the route is rechecked successfully or the operator chooses a ready
|
||||
alternative.
|
||||
Operator interruption is likewise chat-runtime state: Stop and replacement
|
||||
sends must abort the active stream, clear pending tool/approval/question
|
||||
affordances, preserve any partial model text, return focus to the composer,
|
||||
|
||||
@@ -283,12 +283,12 @@ function setViewportWidth(width: number) {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
async function waitForComposerSendReady() {
|
||||
async function waitForProviderCheckSettled() {
|
||||
await waitFor(() => {
|
||||
expect(mockAIAPI.testProvider).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Checking OpenAI provider')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Verifying OpenAI provider')).not.toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Send message' })).not.toBeDisabled();
|
||||
@@ -409,7 +409,7 @@ describe('AIChat', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps user input queued while the selected provider check is still running', async () => {
|
||||
it('sends user input while the selected provider check is still running', async () => {
|
||||
mockAIAPI.getSettings.mockResolvedValue({
|
||||
model: 'openai:gpt-4',
|
||||
chat_model: '',
|
||||
@@ -421,17 +421,21 @@ describe('AIChat', () => {
|
||||
|
||||
renderChat();
|
||||
|
||||
await screen.findByText('Checking OpenAI provider');
|
||||
await screen.findByText('Verifying OpenAI provider');
|
||||
const textarea = screen.getByPlaceholderText(
|
||||
'Ask about your infrastructure...',
|
||||
) as HTMLTextAreaElement;
|
||||
fireEvent.input(textarea, { target: { value: 'summarize the cluster' } });
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Send message' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Send message' })).not.toBeDisabled();
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
|
||||
expect(mockChat.sendMessage).not.toHaveBeenCalled();
|
||||
expect(textarea.value).toBe('summarize the cluster');
|
||||
expect(mockChat.sendMessage).toHaveBeenCalledWith(
|
||||
'summarize the cluster',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(textarea.value).toBe('');
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
});
|
||||
|
||||
@@ -2074,7 +2078,7 @@ describe('AIChat', () => {
|
||||
expect(textarea.value).toBe('@redacted by policy ');
|
||||
});
|
||||
|
||||
await waitForComposerSendReady();
|
||||
await waitForProviderCheckSettled();
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
expect(mockChat.sendMessage).toHaveBeenCalledWith(
|
||||
'@redacted by policy',
|
||||
@@ -2280,7 +2284,7 @@ describe('AIChat', () => {
|
||||
|
||||
const textarea = screen.getByPlaceholderText('Ask about your infrastructure...');
|
||||
fireEvent.input(textarea, { target: { value: 'what happened here?' } });
|
||||
await waitForComposerSendReady();
|
||||
await waitForProviderCheckSettled();
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
|
||||
expect(mockChat.sendMessage).toHaveBeenCalledWith(
|
||||
@@ -2361,7 +2365,7 @@ describe('AIChat', () => {
|
||||
expect(mockAiChatStore.context.briefing?.title).toBe('Patrol finding on web-server');
|
||||
|
||||
fireEvent.input(textarea, { target: { value: 'what changed next?' } });
|
||||
await waitForComposerSendReady();
|
||||
await waitForProviderCheckSettled();
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -554,9 +554,9 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const providerReadinessBlocksSend = createMemo(() => {
|
||||
const providerReadinessHasBlockingError = createMemo(() => {
|
||||
const readiness = providerReadiness();
|
||||
if (readiness.status !== 'checking' && readiness.status !== 'error') return false;
|
||||
if (readiness.status !== 'error') return false;
|
||||
return providerReadinessMatchesSelection(readiness);
|
||||
});
|
||||
|
||||
@@ -591,7 +591,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
provider,
|
||||
model,
|
||||
message: 'Provider check failed',
|
||||
summary: 'Pulse could not verify the selected provider before this chat sends work.',
|
||||
summary: 'Pulse could not verify the selected provider route.',
|
||||
recommendation:
|
||||
'Check provider settings and network reachability, then retry the provider check.',
|
||||
action: 'open_provider_settings',
|
||||
@@ -1174,7 +1174,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
const handleSubmit = () => {
|
||||
const prompt = input().trim();
|
||||
if (!prompt) return;
|
||||
if (providerReadinessBlocksSend()) {
|
||||
if (providerReadinessHasBlockingError()) {
|
||||
focusComposer();
|
||||
return;
|
||||
}
|
||||
@@ -2041,7 +2041,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</Show>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!input().trim() || providerReadinessBlocksSend()}
|
||||
disabled={!input().trim() || providerReadinessHasBlockingError()}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
title="Send"
|
||||
aria-label="Send message"
|
||||
|
||||
@@ -93,6 +93,6 @@ describe('aiChatPresentation', () => {
|
||||
status: 'checking',
|
||||
providerLabel: 'OpenRouter',
|
||||
}).title,
|
||||
).toBe('Checking OpenRouter provider');
|
||||
).toBe('Verifying OpenRouter provider');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,15 +85,15 @@ export function getAIChatProviderReadinessPresentation(args: {
|
||||
if (args.status === 'checking') {
|
||||
return {
|
||||
tone: 'checking',
|
||||
title: `Checking ${providerLabel} provider`,
|
||||
body: 'Pulse is verifying the selected provider before this chat sends work.',
|
||||
title: `Verifying ${providerLabel} provider`,
|
||||
body: 'Pulse is checking the selected provider route in the background.',
|
||||
};
|
||||
}
|
||||
|
||||
const body =
|
||||
args.summary?.trim() ||
|
||||
args.message?.trim() ||
|
||||
'Pulse could not verify the selected provider before this chat sends work.';
|
||||
'Pulse could not verify the selected provider route.';
|
||||
const recommendation = args.recommendation?.trim() || undefined;
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user