diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index e8f5ca1739..e7984bde59 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -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(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 = Promise.resolve(); @@ -3871,12 +3880,7 @@ async function openExternalUrl(url: string) { - diff --git a/apps/desktop/src/lib/ai/__tests__/aiPromptSubmission.spec.ts b/apps/desktop/src/lib/ai/__tests__/aiPromptSubmission.spec.ts new file mode 100644 index 0000000000..057c4db829 --- /dev/null +++ b/apps/desktop/src/lib/ai/__tests__/aiPromptSubmission.spec.ts @@ -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[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/); + }); +}); diff --git a/apps/desktop/src/lib/ai/aiPromptKeyboard.ts b/apps/desktop/src/lib/ai/aiPromptKeyboard.ts index 8c3bf2ee10..0503182cc2 100644 --- a/apps/desktop/src/lib/ai/aiPromptKeyboard.ts +++ b/apps/desktop/src/lib/ai/aiPromptKeyboard.ts @@ -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"; }