diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 8b08a11b1..19f8852d1 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -16,6 +16,7 @@ import { Copy, Database, FileCode, + FileDown, FlaskConical, GitBranch, HelpCircle, @@ -86,6 +87,8 @@ import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/ import { looksLikeActionProposal, containsChinese, looksLikeWriteSqlProposal, shouldGrantWriteSqlOnShortAffirmative } from "@/lib/ai/aiProposalDetect"; import { visibleToActualIndex } from "@/lib/ai/aiMessageEdit"; import { shouldShowReasoningCharCount, reasoningCharCountClass } from "@/lib/ai/aiReasoningPresentation"; +import { saveTextFile } from "@/lib/export/saveTextFile"; +import { buildAiAnalysisExport } from "@/lib/export/aiAnalysisExport"; const { t } = useI18n(); const settings = useSettingsStore(); @@ -121,12 +124,16 @@ type AiMessageMention = interface ChatMessage { role: "user" | "assistant"; content: string; + /** Connection that produced this assistant response; ephemeral export metadata. */ + sourceConnectionName?: string; mentions?: AiMessageMention[]; reasoning?: string; isThinking?: boolean; agentSteps?: AiAgentStepItem[]; /** Hidden system-generated context summary; not rendered in chat UI but included in LLM history. */ kind?: "contextSummary"; + /** Per-message token stats from the last agent run; ephemeral, not persisted. */ + tokens?: { input: number; output: number }; } const props = defineProps<{ @@ -229,7 +236,6 @@ const userPausedAutoScroll = ref(false); const showScrollToBottom = ref(false); const promptCompositionActive = ref(false); const shikiCodeHighlighter = ref(); -const agentTokens = ref<{ input: number; output: number } | null>(null); const promptHistory = ref([]); const historyIndex = ref(-1); const draftBeforeHistory = ref(""); @@ -1741,12 +1747,11 @@ async function send() { confirmedWriteSqlText = undefined; confirmedConnectionId = undefined; confirmedDatabase = undefined; - messages.value.push({ role: "assistant", content: "" }); + messages.value.push({ role: "assistant", content: "", sourceConnectionName: connection.name }); const assistantIdx = messages.value.length - 1; const sessionId = uuid(); currentSessionId.value = sessionId; const agentEvents: AgentEvent[] = []; - agentTokens.value = null; try { const sqlFiles = await loadReferencedSqlFiles(selectedSqlFiles); const context = await buildAiContext(tab, connection, { @@ -1777,7 +1782,8 @@ async function send() { } if (event.type === "agent_end") { if (event.input_tokens || event.output_tokens) { - agentTokens.value = { input: event.input_tokens ?? 0, output: event.output_tokens ?? 0 }; + const msg = messages.value[assistantIdx]; + if (msg) msg.tokens = { input: event.input_tokens ?? 0, output: event.output_tokens ?? 0 }; } } if (event.type === "context_compacted") { @@ -1899,6 +1905,24 @@ async function copyCode(code: string, key: string) { } } +async function exportMessageAsMarkdown(msg: ChatMessage) { + if (!msg.content) return; + + try { + const result = buildAiAnalysisExport({ + connectionName: msg.sourceConnectionName ?? props.connection?.name, + content: msg.content, + analysisLabel: t("ai.analysis"), + dateLabel: new Date().toLocaleString(), + }); + if (!result) return; + await saveTextFile(result.markdown, result.defaultFileName, "Markdown", "md"); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + toast(t("grid.exportFailed", { message }), 5000); + } +} + function clearMessages() { messages.value = []; conversationId.value = ""; @@ -1940,11 +1964,11 @@ function selectConversation(conv: AiConversation) { messages.value = conv.messages.map((m) => ({ role: m.role as "user" | "assistant", content: m.content, + sourceConnectionName: m.role === "assistant" ? conv.connectionName : undefined, mentions: Array.isArray(m.mentions) ? (m.mentions as AiMessageMention[]) : undefined, reasoning: m.reasoning, kind: m.kind, })); - agentTokens.value = null; pendingCompaction.value = null; showConversationList.value = false; scrollToBottom({ force: true }); @@ -2222,8 +2246,9 @@ async function openExternalUrl(url: string) { -
-
+ +
+
+
+ ↑{{ msg.tokens.input.toLocaleString() }} ↓{{ msg.tokens.output.toLocaleString() }} tokens + + +
@@ -2317,9 +2349,6 @@ async function openExternalUrl(url: string) { {{ t("ai.thinking") }}
-
- ↑{{ agentTokens.input.toLocaleString() }} ↓{{ agentTokens.output.toLocaleString() }} tokens -