mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 03:05:44 +00:00
Auto-send on action-style Assistant entry points
Pulse Assistant has been reactive: clicking Explain on a Patrol finding opened the drawer, pre-filled a substantive investigation prompt, and then waited for the operator to press Enter. The captured Pulse Intelligence direction calls for the opposite — when the operator clicks an action button, the analysis should already be in flight by the time they land on the drawer, not parked on a textarea waiting for a second confirmation step. Adds autoSendInitialPrompt to AIChatContext. When true, the chat surface fires handleSubmit immediately after the initialPrompt is written to the input (deferred via queueMicrotask so the input signal has propagated and the drawer-open effects have settled). handleSubmit's existing guards against empty prompts and concurrent submissions make this safe. clearAutoSendFlag is the symmetric clearer so the flag doesn't persist across opens. Wires Explain to autoSendInitialPrompt: true; Discuss with Assistant stays false because that entry is open-ended by design. The same plumbing is what future action-style entries (Investigate, Verify fix) will route through. Tests: two new aiChat-store cases lock in the flag plumbing and the default-undefined behaviour. All 15 aiChat-store + 70 AIChat-component tests pass. Verified live: clicking Explain clears the textarea and surfaces the in-flight Thinking indicator without the operator pressing Enter.
This commit is contained in:
@@ -589,13 +589,31 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-fill input when opened with an initialPrompt (e.g., "Discuss with Assistant" from findings)
|
||||
// Pre-fill input when opened with an initialPrompt (e.g., "Discuss with
|
||||
// Assistant" from findings). When autoSendInitialPrompt is true,
|
||||
// immediately submit instead of waiting for the operator to press Enter
|
||||
// — used by action-style entry points like "Explain" where clicking the
|
||||
// button is itself the operator's decision to invoke the action.
|
||||
createEffect(() => {
|
||||
const ctx = aiChatStore.context;
|
||||
if (ctx.initialPrompt && isOpen()) {
|
||||
setInput(ctx.initialPrompt);
|
||||
// Clear so it doesn't re-fire on subsequent opens
|
||||
const promptToSend = ctx.initialPrompt;
|
||||
const shouldAutoSend = Boolean(ctx.autoSendInitialPrompt);
|
||||
setInput(promptToSend);
|
||||
// Clear flags so they don't re-fire on subsequent opens
|
||||
aiChatStore.clearInitialPrompt?.();
|
||||
aiChatStore.clearAutoSendFlag?.();
|
||||
if (shouldAutoSend) {
|
||||
// Defer to the next tick so the input signal has propagated to
|
||||
// handleSubmit's `input()` read, and so the drawer mount /
|
||||
// session-load effects can settle. handleSubmit itself guards
|
||||
// against empty prompts and against double-firing while a chat
|
||||
// is in flight, so this is safe.
|
||||
queueMicrotask(() => {
|
||||
if (!isOpen()) return;
|
||||
handleSubmit();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -559,7 +559,16 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
nextStepAction,
|
||||
intent,
|
||||
});
|
||||
aiChatStore.openWithPrompt(handoff.prompt, handoff.context);
|
||||
// Explain is an action-style entry point — clicking the button is the
|
||||
// operator asking "do the explaining for me," not "open chat so I can
|
||||
// type a question." Auto-submit the handoff prompt so the analysis is
|
||||
// already in flight when the drawer is on screen. Discuss with
|
||||
// Assistant keeps the previous behaviour (pre-fill, operator drives)
|
||||
// because that entry is open-ended by intent.
|
||||
aiChatStore.openWithPrompt(handoff.prompt, {
|
||||
...handoff.context,
|
||||
autoSendInitialPrompt: intent === 'explain',
|
||||
});
|
||||
};
|
||||
|
||||
const handleDiscussWithAssistant = async (finding: UnifiedFinding, e: Event) => {
|
||||
|
||||
@@ -135,6 +135,37 @@ describe('aiChatStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('threads autoSendInitialPrompt through openWithPrompt and clears it on clearAutoSendFlag', () => {
|
||||
// Action-style entry points (Explain, Investigate, eventually Verify
|
||||
// fix) set autoSendInitialPrompt: true so the chat surface submits
|
||||
// immediately on open instead of pre-filling the input and waiting
|
||||
// for Enter. Discuss-style entries leave it falsy.
|
||||
aiChatStore.openWithPrompt('Explain this Patrol finding', {
|
||||
targetType: 'vm',
|
||||
targetId: 'vm-100',
|
||||
autoSendInitialPrompt: true,
|
||||
});
|
||||
|
||||
expect(aiChatStore.context.initialPrompt).toBe('Explain this Patrol finding');
|
||||
expect(aiChatStore.context.autoSendInitialPrompt).toBe(true);
|
||||
|
||||
aiChatStore.clearAutoSendFlag();
|
||||
|
||||
expect(aiChatStore.context.autoSendInitialPrompt).toBeUndefined();
|
||||
// The initialPrompt is independent — clearAutoSendFlag must not also
|
||||
// clear the prompt itself (the chat effect still needs to read and
|
||||
// submit it before its own clearInitialPrompt runs).
|
||||
expect(aiChatStore.context.initialPrompt).toBe('Explain this Patrol finding');
|
||||
});
|
||||
|
||||
it('defaults autoSendInitialPrompt to undefined when not provided', () => {
|
||||
aiChatStore.openWithPrompt('discuss this', {
|
||||
targetType: 'vm',
|
||||
targetId: 'vm-200',
|
||||
});
|
||||
expect(aiChatStore.context.autoSendInitialPrompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves scoped autonomous-mode overrides for pre-filled prompts', () => {
|
||||
aiChatStore.openWithPrompt('brief me', {
|
||||
targetType: 'dashboard',
|
||||
|
||||
@@ -83,6 +83,15 @@ export interface AIChatContext {
|
||||
handoffMetadata?: AIChatHandoffMetadata;
|
||||
// Per-request execution mode override; false keeps scoped handoffs approval-required.
|
||||
autonomousMode?: boolean;
|
||||
// When true, the chat surface should auto-submit the initialPrompt as
|
||||
// soon as the drawer opens, rather than just pre-filling the input and
|
||||
// waiting for the operator to press Enter. Used by action-style entry
|
||||
// points like "Explain" / "Investigate" where the operator has already
|
||||
// decided to invoke the action by clicking the button — leaving them
|
||||
// to also press Enter is friction. Discuss-style entry points where
|
||||
// the operator is opening chat to drive the conversation themselves
|
||||
// should leave this false (the default).
|
||||
autoSendInitialPrompt?: boolean;
|
||||
}
|
||||
|
||||
// A single context item that can be accumulated
|
||||
@@ -484,6 +493,16 @@ export const aiChatStore = {
|
||||
});
|
||||
},
|
||||
|
||||
// Clear the auto-send flag after firing, so a subsequent open of the
|
||||
// drawer without an explicit autoSend doesn't accidentally re-fire.
|
||||
clearAutoSendFlag() {
|
||||
setAIChatContext((prev) => {
|
||||
if (!prev.autoSendInitialPrompt) return prev;
|
||||
const { autoSendInitialPrompt: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
},
|
||||
|
||||
// Clear the findingId after first message is sent
|
||||
clearFindingId() {
|
||||
setAIChatContext((prev) => {
|
||||
|
||||
Reference in New Issue
Block a user