diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index f0701f751..2d0b68469 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -21,8 +21,8 @@ import type { ParamDef, ParamType } from '@open-pencil/core' const require = createRequire(import.meta.url) const MCP_VERSION: string = (require('../package.json') as { version: string }).version -type McpContent = { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string } -type McpResult = { content: McpContent[]; isError?: boolean } +type MCPContent = { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string } +type MCPResult = { content: MCPContent[]; isError?: boolean } const RPC_TIMEOUT = 30_000 @@ -32,11 +32,11 @@ interface PendingRequest { timer: ReturnType } -function ok(data: unknown): McpResult { +function ok(data: unknown): MCPResult { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] } } -function fail(e: unknown): McpResult { +function fail(e: unknown): MCPResult { const msg = e instanceof Error ? e.message : String(e) return { content: [{ type: 'text', text: JSON.stringify({ error: msg }) }], isError: true } } @@ -127,7 +127,6 @@ export function startServer(options: ServerOptions = {}) { } catch (e) { console.warn('Malformed automation message:', e) } - } function rejectAllPending(reason: string) { @@ -235,11 +234,11 @@ export function startServer(options: ServerOptions = {}) { // --- MCP Streamable HTTP --- - type McpTransport = { handleRequest: (r: Request) => Promise } - const mcpSessions = new Map() + type MCPTransport = { handleRequest: (r: Request) => Promise } + const mcpSessions = new Map() const MAX_MCP_SESSIONS = 10 - function createMcpSession(id: string): McpTransport { + function createMCPSession(id: string): MCPTransport { const mcpServer = new McpServer({ name: 'open-pencil', version: MCP_VERSION }) const register = mcpServer.registerTool.bind(mcpServer) as (...a: unknown[]) => void @@ -304,7 +303,7 @@ export function startServer(options: ServerOptions = {}) { { status: 503, headers: { 'Retry-After': '5' } } ) } - const transport = existing ?? createMcpSession(sessionId ?? randomUUID()) + const transport = existing ?? createMCPSession(sessionId ?? randomUUID()) const response = await transport.handleRequest(c.req.raw) if (c.req.method === 'DELETE' && sessionId) { mcpSessions.delete(sessionId) diff --git a/src/ai/acp-map-update.ts b/src/ai/acp-map-update.ts index 98881ab66..3460054b6 100644 --- a/src/ai/acp-map-update.ts +++ b/src/ai/acp-map-update.ts @@ -90,11 +90,13 @@ export function textFromContent( content: Record[] | undefined ): string | undefined { if (!content) return undefined - return content - .filter( - (c) => - c.type === 'content' && (c.content as Record | undefined)?.type === 'text' - ) - .map((c) => (c.content as Record).text) - .join('\n') + const parts: string[] = [] + for (const c of content) { + if (c.type !== 'content') continue + const inner = c.content as Record | undefined + if (inner?.type === 'text' && typeof inner.text === 'string') { + parts.push(inner.text) + } + } + return parts.length > 0 ? parts.join('\n') : undefined } diff --git a/src/ai/acp-transport.ts b/src/ai/acp-transport.ts index 5ab8d3e1c..5e1e546f4 100644 --- a/src/ai/acp-transport.ts +++ b/src/ai/acp-transport.ts @@ -56,15 +56,16 @@ export function buildCrashChunks( return { chunks, shouldNullSession: true } } -interface AcpDebugEntry { +interface ACPDebugEntry { ts: number type: string data: unknown } const MAX_LOG_AGE_MS = 5 * 60 * 1000 +const IS_DEV = import.meta.env.DEV -export const acpDebugLog: AcpDebugEntry[] = [] +export const acpDebugLog: ACPDebugEntry[] = [] function pruneOldEntries() { const cutoff = Date.now() - MAX_LOG_AGE_MS @@ -75,29 +76,18 @@ function pruneOldEntries() { export function getAcpDebugText(): string { pruneOldEntries() - return acpDebugLog.map((e) => - `[${new Date(e.ts).toISOString()}] ${e.type}\n${JSON.stringify(e.data, null, 2)}` - ).join('\n\n---\n\n') + return acpDebugLog + .map((e) => `[${new Date(e.ts).toISOString()}] ${e.type}\n${JSON.stringify(e.data, null, 2)}`) + .join('\n\n---\n\n') } export function clearAcpDebugLog() { acpDebugLog.length = 0 } -export async function saveAcpDebugLog(): Promise { +export function hasAcpDebugEntries(): boolean { pruneOldEntries() - const text = getAcpDebugText() - if (!text) return null - try { - const { writeTextFile } = await import('@tauri-apps/plugin-fs') - const { join, desktopDir } = await import('@tauri-apps/api/path') - const filename = `acp-debug-${new Date().toISOString().replace(/[:.]/g, '-')}.log` - const path = await join(await desktopDir(), filename) - await writeTextFile(path, text) - return path - } catch { - return null - } + return acpDebugLog.length > 0 } export class ACPChatTransport implements ChatTransport { @@ -158,11 +148,13 @@ export class ACPChatTransport implements ChatTransport { session.onUpdate = (params) => { if (closed) return - acpDebugLog.push({ - ts: Date.now(), - type: params.update.sessionUpdate, - data: params.update - }) + if (IS_DEV) { + acpDebugLog.push({ + ts: Date.now(), + type: params.update.sessionUpdate, + data: params.update + }) + } const result = mapUpdate(params.update, textId, textStarted) for (const chunk of result.chunks) { controller.enqueue(chunk) diff --git a/src/automation/spawn-mcp.ts b/src/automation/spawn-mcp.ts index 8b3291b53..9651fd019 100644 --- a/src/automation/spawn-mcp.ts +++ b/src/automation/spawn-mcp.ts @@ -19,7 +19,7 @@ async function pollHealth(retries: number, delayMs: number): Promise { return false } -export async function spawnMcpIfNeeded(): Promise<(() => void) | null> { +export async function spawnMCPIfNeeded(): Promise<(() => void) | null> { if (import.meta.env.DEV || !IS_TAURI) return null if (await checkHealth()) return null diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue index 98fe45154..89087ec83 100644 --- a/src/components/ChatPanel.vue +++ b/src/components/ChatPanel.vue @@ -2,10 +2,10 @@ import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui' import { computed, markRaw, nextTick, ref, watch } from 'vue' -import { getAcpDebugText, clearAcpDebugLog, saveAcpDebugLog } from '@/ai/acp-transport' +import { getAcpDebugText, clearAcpDebugLog, hasAcpDebugEntries } from '@/ai/acp-transport' import { copyChatLog } from '@/ai/chat-debug' import { clearToolLogEntries, didHitStepLimit } from '@/ai/tools' -import AcpPermissionDialog from '@/components/chat/AcpPermissionDialog.vue' +import ACPPermissionDialog from '@/components/chat/ACPPermissionDialog.vue' import ChatInput from '@/components/chat/ChatInput.vue' import ChatMessage from '@/components/chat/ChatMessage.vue' import ProviderSetup from '@/components/chat/ProviderSetup.vue' @@ -92,9 +92,10 @@ async function handleCopyAcpLog() { const text = getAcpDebugText() if (!text) return await navigator.clipboard.writeText(text) - void saveAcpDebugLog() acpLogCopied.value = true - setTimeout(() => { acpLogCopied.value = false }, 1500) + setTimeout(() => { + acpLogCopied.value = false + }, 1500) } function handleClearChat() { @@ -183,6 +184,7 @@ function handleClearChat() { {{ debugCopied ? 'Copied' : 'Copy log' }}