From b3f2732dff0d3f5e9a3f09ca522c6f9a507bc2e2 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 17 Apr 2026 18:46:13 +0300 Subject: [PATCH] fix(vscode): make session markdown export save/reveal work --- packages/ui/src/lib/exportSession.ts | 33 +++++++++++++++-- packages/vscode/src/bridge-fs-runtime.ts | 45 ++++++++++++++++++++++++ packages/vscode/webview/api/files.ts | 6 ++++ packages/vscode/webview/main.tsx | 12 +++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/lib/exportSession.ts b/packages/ui/src/lib/exportSession.ts index fa4aa263..bee12c5a 100644 --- a/packages/ui/src/lib/exportSession.ts +++ b/packages/ui/src/lib/exportSession.ts @@ -1,6 +1,6 @@ import type { Message, Part } from '@opencode-ai/sdk/v2'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; -import { openDesktopPath, saveDesktopMarkdownFile } from '@/lib/desktop'; +import { isVSCodeRuntime, openDesktopPath, saveDesktopMarkdownFile } from '@/lib/desktop'; import { getRevealLabel } from '@/lib/utils'; type SessionMessageRecord = { info: Message; parts: Part[] }; @@ -95,7 +95,36 @@ export function downloadAsMarkdown(content: string, filename: string): void { } export async function saveAsMarkdownDesktop(content: string, filename: string): Promise { - return saveDesktopMarkdownFile(filename, content); + const desktopPath = await saveDesktopMarkdownFile(filename, content); + if (desktopPath) { + return desktopPath; + } + + if (!isVSCodeRuntime()) { + return null; + } + + try { + const response = await fetch('/api/vscode/save-markdown', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fileName: filename, content }), + }); + + if (!response.ok) { + return null; + } + + const payload = await response.json() as { saved?: boolean; path?: string }; + if (payload.saved !== true) { + return null; + } + + const savedPath = typeof payload.path === 'string' ? payload.path.trim() : ''; + return savedPath.length > 0 ? savedPath : null; + } catch { + return null; + } } export async function revealExportedMarkdown(path: string): Promise { diff --git a/packages/vscode/src/bridge-fs-runtime.ts b/packages/vscode/src/bridge-fs-runtime.ts index d2827f35..5f0ab712 100644 --- a/packages/vscode/src/bridge-fs-runtime.ts +++ b/packages/vscode/src/bridge-fs-runtime.ts @@ -446,6 +446,51 @@ export async function handleFsBridgeMessage( return { id, type, success: true, data: { saved: true, path: saveUri.fsPath || saveUri.toString() } }; } + case 'api:files/save-markdown': { + const rawFileName = (payload as { fileName?: unknown })?.fileName; + const rawContent = (payload as { content?: unknown })?.content; + const content = typeof rawContent === 'string' ? rawContent : ''; + if (!content) { + return { id, type, success: false, error: 'Invalid markdown payload' }; + } + + const defaultFileName = typeof rawFileName === 'string' && rawFileName.trim().length > 0 + ? rawFileName.trim() + : `session-${Date.now()}.md`; + + const saveUri = await vscode.window.showSaveDialog({ + saveLabel: 'Export session', + defaultUri: vscode.workspace.workspaceFolders?.[0] + ? vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, defaultFileName) + : undefined, + filters: { Markdown: ['md'] }, + }); + + if (!saveUri) { + return { id, type, success: true, data: { saved: false, canceled: true } }; + } + + await vscode.workspace.fs.writeFile(saveUri, Buffer.from(content, 'utf8')); + + return { id, type, success: true, data: { saved: true, path: saveUri.fsPath || saveUri.toString() } }; + } + + case 'api:fs:reveal': { + const targetPath = (payload as { path?: unknown })?.path; + const value = typeof targetPath === 'string' ? targetPath.trim() : ''; + if (!value) { + return { id, type, success: false, error: 'Path is required' }; + } + + try { + const uri = value.includes('://') ? vscode.Uri.parse(value) : vscode.Uri.file(value); + await vscode.commands.executeCommand('revealFileInOS', uri); + return { id, type, success: true, data: { success: true } }; + } catch { + return { id, type, success: false, error: 'Failed to reveal path' }; + } + } + default: return null; } diff --git a/packages/vscode/webview/api/files.ts b/packages/vscode/webview/api/files.ts index 96033c0e..d93562f9 100644 --- a/packages/vscode/webview/api/files.ts +++ b/packages/vscode/webview/api/files.ts @@ -113,6 +113,12 @@ export const createVSCodeFilesAPI = (): FilesAPI => ({ }; }, + async revealPath(path: string): Promise<{ success: boolean }> { + const target = normalizePath(path); + const data = await sendBridgeMessage<{ success?: boolean }>('api:fs:reveal', { path: target }); + return { success: Boolean(data?.success) }; + }, + async execCommands(commands: string[], cwd: string): Promise<{ success: boolean; results: CommandExecResult[] }> { const targetCwd = normalizePath(cwd); const data = await sendBridgeMessageWithOptions<{ success: boolean; results?: CommandExecResult[] }>('api:fs:exec', { diff --git a/packages/vscode/webview/main.tsx b/packages/vscode/webview/main.tsx index 2bedff30..92759244 100644 --- a/packages/vscode/webview/main.tsx +++ b/packages/vscode/webview/main.tsx @@ -589,6 +589,18 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => { return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }); } + if (pathname.startsWith('/api/vscode/save-markdown') && method === 'POST') { + const body = init?.body ? JSON.parse(init.body as string) : {}; + const fileName = typeof (body as { fileName?: unknown }).fileName === 'string' + ? (body as { fileName: string }).fileName + : undefined; + const content = typeof (body as { content?: unknown }).content === 'string' + ? (body as { content: string }).content + : undefined; + const data = await sendBridgeMessage('api:files/save-markdown', { fileName, content }); + return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + if (pathname.startsWith('/api/config/agents/')) { const encodedName = pathname.slice('/api/config/agents/'.length); const name = decodeURIComponent(encodedName);