fix(ai): align send button with prompt submission

Closes #6949
This commit is contained in:
t8y2 committed 2026-08-22 09:31:12 +00:00
1 parent 756d4aedc9
commit 3f1544d45e
3 files changed
+59 -7

No files matched your search

@@ -133,7 +133,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
import { AI_TABLE_MENTION_CANDIDATE_LIMIT, AI_TABLE_MENTION_SCHEMA_LIMIT, filterAiTableMentionCandidates, formatAiTableMention, parseAiTableMentions, type AiTableMention } from "@/lib/ai/aiTableMentions";
import { handleAiTableReferenceDropEvent } from "@/lib/ai/aiTableReferenceDrop";
import { DBX_TABLE_REFERENCE_DROP_EVENT, clearActiveTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/ai/aiPromptKeyboard";
import { canSubmitAiPrompt, isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/ai/aiPromptKeyboard";
import { isActionableWriteProposalMessage, isActionableWriteSqlProposal, looksLikeActionProposal, looksLikeWriteSqlProposal, shouldGrantWriteSqlOnShortAffirmative } from "@/lib/ai/aiProposalDetect";
import { visibleToActualIndex } from "@/lib/ai/aiMessageEdit";
import { shouldShowReasoningCharCount, reasoningCharCountClass } from "@/lib/ai/aiReasoningPresentation";
@@ -728,6 +728,15 @@ const previewImageAttachment = ref<AiImageAttachment | null>(null);
const isAttachmentDragging = ref(false);
const pendingAttachmentReads = ref(0);
const isAttachmentProcessing = computed(() => pendingAttachmentReads.value > 0);
const canSubmitPrompt = computed(() =>
canSubmitAiPrompt({
prompt: prompt.value,
contextItemCount: selectedMentions.value.length + selectedSqlFileMentions.value.length + selectedCsvAttachments.value.length + selectedImageAttachments.value.length,
isAttachmentProcessing: isAttachmentProcessing.value,
hasTab: !!props.tab,
hasConnection: !!props.connection,
}),
);
let browserAttachmentDragDepth = 0;
let attachmentDraftEpoch = 0;
let attachmentReadQueue: Promise<void> = Promise.resolve();
@@ -3871,12 +3880,7 @@ async function openExternalUrl(url: string) {
<button v-if="isGenerating" class="h-7 w-7 shrink-0 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center" :title="t('ai.stopGenerating')" @click="cancelStream">
<Square class="h-3.5 w-3.5" />
</button>
<button
v-else
class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background flex items-center justify-center disabled:opacity-30"
:disabled="isAttachmentProcessing || (!prompt.trim() && !selectedMentions.length && !selectedSqlFileMentions.length && !selectedCsvAttachments.length && !selectedImageAttachments.length) || !props.tab?.database"
@click="send"
>
<button v-else class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background flex items-center justify-center disabled:opacity-30" :disabled="!canSubmitPrompt" @click="send">
<ArrowUp class="h-4 w-4" />
</button>
</div>
@@ -0,0 +1,36 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { canSubmitAiPrompt } from "@/lib/ai/aiPromptKeyboard";
const aiAssistantSource = readFileSync(new URL("../../../components/editor/AiAssistant.vue", import.meta.url), "utf8");
function submissionState(overrides: Partial<Parameters<typeof canSubmitAiPrompt>[0]> = {}) {
return {
prompt: "",
contextItemCount: 0,
isAttachmentProcessing: false,
hasTab: true,
hasConnection: true,
...overrides,
};
}
describe("AI prompt submission eligibility", () => {
it("allows text and attachment submissions without requiring a tab-local database", () => {
expect(canSubmitAiPrompt(submissionState({ prompt: "show current users" }))).toBe(true);
expect(canSubmitAiPrompt(submissionState({ contextItemCount: 1 }))).toBe(true);
});
it("rejects empty, processing, and missing-context submissions", () => {
expect(canSubmitAiPrompt(submissionState())).toBe(false);
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", isAttachmentProcessing: true }))).toBe(false);
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", hasTab: false }))).toBe(false);
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", hasConnection: false }))).toBe(false);
});
it("wires the click button to the shared eligibility rule", () => {
expect(aiAssistantSource).toContain("const canSubmitPrompt = computed(() =>");
expect(aiAssistantSource).toContain(':disabled="!canSubmitPrompt"');
expect(aiAssistantSource).not.toMatch(/:disabled=.*props\.tab\?\.database/);
});
});
@@ -5,6 +5,18 @@ export interface AiPromptKeydownLikeEvent {
keyCode?: number;
}
export interface AiPromptSubmissionState {
prompt: string;
contextItemCount: number;
isAttachmentProcessing: boolean;
hasTab: boolean;
hasConnection: boolean;
}
export function canSubmitAiPrompt(state: AiPromptSubmissionState): boolean {
return !state.isAttachmentProcessing && state.hasTab && state.hasConnection && (state.prompt.trim().length > 0 || state.contextItemCount > 0);
}
export function isAiPromptImeCompositionEvent(event: AiPromptKeydownLikeEvent, compositionActive = false): boolean {
return compositionActive || !!event.isComposing || event.keyCode === 229 || event.key === "Process";
}