From 6182adb01ccff11ff63c0c0eaedde5cf3bbe4672 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 23:13:10 +0000 Subject: [PATCH] feat(chat): add conversation export & three-dot menu (M8.7) Export conversations as Markdown, JSON, or plain text via three-dot menu in chat header. Uses File System Access API with fallback. Also adds delete conversation option. Co-Authored-By: Claude Opus 4.6 --- .../app/src/components/chat/ChatHeader.vue | 84 +++++++++++++++++ packages/app/src/utils/conversation-export.ts | 90 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 packages/app/src/utils/conversation-export.ts diff --git a/packages/app/src/components/chat/ChatHeader.vue b/packages/app/src/components/chat/ChatHeader.vue index fd333cf8..91324649 100644 --- a/packages/app/src/components/chat/ChatHeader.vue +++ b/packages/app/src/components/chat/ChatHeader.vue @@ -45,6 +45,17 @@ + + + + +
+ +
+ + + @@ -157,6 +209,7 @@ import { ref, computed, watch, nextTick } from 'vue' import { useChatStore } from '@/stores/chat' import { useAI } from '@/composables/useAI' import { useContentPanel } from '@/composables/useContentPanel' +import { downloadConversation, type ExportFormat } from '@/utils/conversation-export' defineProps<{ title: string @@ -176,6 +229,9 @@ const chatStore = useChatStore() const webSearchEnabled = computed(() => chatStore.webSearchEnabled) const showModelPicker = ref(false) const showChatList = ref(false) +const showMenu = ref(false) +const menuTriggerRef = ref(null) +const menuDropdownStyle = ref>({}) const headerRef = ref(null) const chatListTriggerRef = ref(null) const modelPickerTriggerRef = ref(null) @@ -211,8 +267,22 @@ function updateModelPickerPosition() { }) } +function updateMenuPosition() { + nextTick(() => { + const el = menuTriggerRef.value + if (el) { + const r = el.getBoundingClientRect() + menuDropdownStyle.value = { + top: `${r.bottom + 4}px`, + right: `${window.innerWidth - r.right}px`, + } + } + }) +} + watch(showChatList, (v) => { if (v) updateChatListPosition() }) watch(showModelPicker, (v) => { if (v) updateModelPickerPosition() }) +watch(showMenu, (v) => { if (v) updateMenuPosition() }) const conversationList = computed(() => chatStore.conversationList) const activeConversationId = computed(() => chatStore.activeConversationId) @@ -246,6 +316,20 @@ function openDesignSystem() { enterDesignSystemMode() showModelPicker.value = false } + +async function handleExport(format: ExportFormat) { + const conv = chatStore.activeConversation + if (!conv) return + await downloadConversation(conv, format) + showMenu.value = false +} + +function handleDelete() { + const id = chatStore.activeConversationId + if (!id) return + chatStore.deleteConversation(id) + showMenu.value = false +}