diff --git a/.github/workflows/chat_kernel_ui.yml b/.github/workflows/chat_kernel_ui.yml new file mode 100644 index 000000000..a0efe3310 --- /dev/null +++ b/.github/workflows/chat_kernel_ui.yml @@ -0,0 +1,92 @@ +name: Chat Kernel and UI gates + +on: + workflow_dispatch: + pull_request: + paths: + - 'renderer.js' + - 'main.html' + - 'modules/**' + - 'preloads/**' + - 'styles/**' + - 'tests/**' + - 'scripts/**' + - 'docs/chat-kernel-*.md' + - 'docs/contracts/**' + - 'docs/next-ui-current-state.md' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/chat_kernel_ui.yml' + +permissions: + contents: read + +jobs: + contracts: + name: Chat Kernel and UI contracts + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + + - name: Setup Node 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: npm + + - name: Setup Rust for built-artifact evidence + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable 2026-08-11 + + - name: Install dependencies (including Electron runtime) + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + # Electron's postinstall script downloads the runtime used by the + # embedded-app contract tests. Do not suppress lifecycle scripts here; + # this job does not perform native addon rebuilds. + run: npm ci --omit=optional + + - name: Verify Electron runtime + run: node -e "const electron = require('electron'); if (typeof electron !== 'string') { throw new Error('Electron runtime path was not installed'); } console.log(electron)" + + - name: Run Chat Kernel tests + run: npm run test:chat-kernel + + - name: Run Chat contract evidence gate + env: + VCPCHAT_PACKAGED_ARTIFACT_SMOKE_OUTPUT: ${{ runner.temp }}/packaged-artifact-smoke.json + run: npm run check:chat-evidence + + - name: Write Chat evidence manifest + env: + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: ${{ runner.temp }}/packaged-artifact-smoke.json + run: npm run write:chat-evidence-manifest -- "$RUNNER_TEMP/chat-evidence-manifest.json" + + - name: Publish Chat evidence summary + env: + CHAT_EVIDENCE_MANIFEST: ${{ runner.temp }}/chat-evidence-manifest.json + run: node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync(process.env.CHAT_EVIDENCE_MANIFEST,'utf8')); fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, ['## Chat evidence','', '- status: `'+m.status+'`','- contracts/events: `'+m.checks.contracts+'/'+m.checks.generatedEvents+'`','- dynamic sites: registered `'+m.checks.dynamicSites.registered+'`, undiscovered `'+m.checks.dynamicSites.undiscovered+'`','- artifact plane: `'+m.checks.artifactPlane+'`','- packaged artifact: `'+m.checks.packagedArtifact+'`','- Windows matrix: `'+m.checks.windowsMatrix+'`','- manual soak: `'+m.checks.manualSoak+'`','- real Electron: `'+m.checks.realElectronSequence+'` (manual/platform evidence remains separate)',''].join('\\n'));" + + - name: Upload Chat evidence manifest + uses: actions/upload-artifact@v4 + with: + name: chat-evidence-manifest + path: | + ${{ runner.temp }}/chat-evidence-manifest.json + ${{ runner.temp }}/packaged-artifact-smoke.json + + - name: Upload Chat evidence bundle + uses: actions/upload-artifact@v4 + with: + name: chat-evidence-bundle + if-no-files-found: error + path: | + docs/contracts/chat-contract.schema.json + docs/contracts/chat-contracts.json + docs/contracts/generated/chat-event-graph.json + docs/contracts/snapshots/*.json + + - name: Run complete UI system gate + run: npm run check:ui-system diff --git a/Flowlockmodules/flowlock-integration.js b/Flowlockmodules/flowlock-integration.js index b77e1284f..f35e250a7 100644 --- a/Flowlockmodules/flowlock-integration.js +++ b/Flowlockmodules/flowlock-integration.js @@ -3,6 +3,10 @@ // 多 Agent 并发架构:续写不再依赖当前 UI 状态,而是按指定 Agent/Topic 后台执行。 console.log('[Flowlock Integration] Loading integration script (multi-session)...'); +let chatManagerProvider = null; +let historyMutationAuthorityProvider = null; +let settingsProvider = null; +const mainChatSnapshot = () => window.VCPMainChatState?.snapshot?.() || { selectedItem: null, topicId: null }; /** * 初始化Flowlock模块 @@ -23,7 +27,7 @@ function initializeFlowlock() { electronAPI: electronAPI, uiHelper: window.uiHelperFunctions, globalSettingsRef: { - get: () => window.globalSettings || {} + get: () => settingsProvider?.get?.() || {} }, continueWritingForContext: continueWritingForContext }); @@ -58,7 +62,7 @@ function normalizeFlowlockHeartbeatPrompt(prompt) { async function continueWritingForContext(params) { const { agentId, topicId, prompt, messageId } = params; const chatAPI = window.chatAPI || window.electronAPI; - const globalSettings = window.globalSettings || {}; + const globalSettings = settingsProvider?.get?.() || {}; if (!agentId || !topicId) { console.error('[Flowlock] continueWritingForContext: missing agentId or topicId'); @@ -211,8 +215,7 @@ async function continueWritingForContext(params) { avatarColor: agentConfig?.avatarCalculatedColor }; - const currentSelectedItem = window.currentSelectedItem; - const currentTopicId = window.currentTopicId; + const { selectedItem: currentSelectedItem, topicId: currentTopicId } = mainChatSnapshot(); const isForCurrentView = currentSelectedItem?.id === agentId && currentTopicId === topicId; // 构建上下文。后台流也必须带完整身份,streamManager 才能写入正确历史。 @@ -225,29 +228,21 @@ async function continueWritingForContext(params) { avatarColor: agentConfig?.avatarCalculatedColor }; - // 流式消息必须在发送请求前初始化,无论它当前是否可见。 - // streamManager 自己负责“当前视图渲染 / 后台只写历史”的分流。 - if (useStreaming) { - const streamStarter = window.streamManager?.startStreamingMessage - || window.messageRenderer?.startStreamingMessage; - if (typeof streamStarter !== 'function') { - throw new Error('流式消息管理器未就绪'); - } - - await streamStarter({ - ...thinkingMessage, - content: '', - agentId, - topicId, - context - }); - } else { + // Streaming lifecycle is now started by the VCP stream bridge when the + // producer publishes its first owned event. This prevents Flowlock from + // creating a second terminal state machine for the same message. + if (!useStreaming) { let historyWithThinking = await chatAPI.getChatHistory(agentId, topicId); if (!historyWithThinking || historyWithThinking.error) { historyWithThinking = []; } historyWithThinking.push(thinkingMessage); - await chatAPI.saveChatHistory(agentId, topicId, historyWithThinking); + await historyMutationAuthorityProvider.replace({ + itemId: agentId, + itemType: 'agent', + topicId, + category: 'flowlock-start' + }, historyWithThinking); } // 发送到 VCP @@ -284,10 +279,15 @@ async function continueWritingForContext(params) { if (historyForSave && !historyForSave.error) { const finalHistory = historyForSave.filter(msg => msg.id !== thinkingMessageId && !msg.isThinking); finalHistory.push(assistantMessage); - await chatAPI.saveChatHistory(agentId, topicId, finalHistory); - - if (isForCurrentView && window.chatManager?.loadChatHistory) { - await window.chatManager.loadChatHistory(agentId, 'agent', topicId); + await historyMutationAuthorityProvider.replace({ + itemId: agentId, + itemType: 'agent', + topicId, + category: 'flowlock-terminal' + }, finalHistory); + + if (isForCurrentView && chatManagerProvider?.loadChatHistory) { + await chatManagerProvider.loadChatHistory(agentId, 'agent', topicId); } } @@ -323,8 +323,7 @@ function setupFlowlockInteractions() { e.preventDefault(); - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (!currentItem || !currentItem.id || !currentTopic) { if (window.uiHelperFunctions?.showToastNotification) { @@ -351,8 +350,7 @@ function setupFlowlockInteractions() { if (!window.flowlockManager) return; - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (window.flowlockManager.isAgentLocked(currentItem.id)) { // 如果已激活,则停止 @@ -384,8 +382,7 @@ function setupFlowlockShortcuts() { if (!window.flowlockManager) return; - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (!currentItem || !currentItem.id || !currentTopic) { if (window.uiHelperFunctions?.showToastNotification) { @@ -410,18 +407,40 @@ function setupFlowlockShortcuts() { /** * 主初始化函数 */ -function initializeFlowlockIntegration() { +function initializeFlowlockIntegration(dependencies = {}) { try { + const provider = dependencies.chatManager; + if (!provider || typeof provider.loadChatHistory !== 'function' || provider.isReady?.() === false) { + throw new TypeError('Flowlock integration requires a ready ChatManager provider.'); + } + const mutationAuthority = dependencies.historyMutationAuthority; + if (!mutationAuthority || typeof mutationAuthority.replace !== 'function') { + throw new TypeError('Flowlock integration requires a history mutation authority.'); + } + if (chatManagerProvider && chatManagerProvider !== provider) { + throw new Error('Flowlock ChatManager provider is already registered.'); + } + if (historyMutationAuthorityProvider && historyMutationAuthorityProvider !== mutationAuthority) { + throw new Error('Flowlock history mutation authority is already registered.'); + } + if (!dependencies.settings || typeof dependencies.settings.get !== 'function') { + throw new TypeError('Flowlock integration requires a settings provider.'); + } + chatManagerProvider = provider; + historyMutationAuthorityProvider = mutationAuthority; + settingsProvider = dependencies.settings; initializeFlowlock(); setupFlowlockInteractions(); setupFlowlockShortcuts(); // 页面卸载时清理 - window.addEventListener('beforeunload', () => { + const cleanup = () => { if (window.flowlockManager) { window.flowlockManager.cleanup(); } - }); + }; + if (dependencies.listenerOwner?.add) dependencies.listenerOwner.add(window, 'beforeunload', cleanup, { once: true }); + else window.addEventListener('beforeunload', cleanup, { once: true }); console.log('[Flowlock Integration] Full integration complete (multi-session).'); } catch (error) { @@ -432,4 +451,4 @@ function initializeFlowlockIntegration() { // 导出到全局作用域 window.initializeFlowlockIntegration = initializeFlowlockIntegration; -console.log('[Flowlock Integration] Integration script loaded (multi-session).'); \ No newline at end of file +console.log('[Flowlock Integration] Integration script loaded (multi-session).'); diff --git a/Flowlockmodules/flowlock.js b/Flowlockmodules/flowlock.js index 8260715ce..242825118 100644 --- a/Flowlockmodules/flowlock.js +++ b/Flowlockmodules/flowlock.js @@ -115,7 +115,7 @@ class FlowlockManager { */ async stop(agentId) { // 兼容旧插件 stop():优先停止当前可见 Agent,不再错误地停止其他并发 Session。 - const resolvedAgentId = agentId || window.currentSelectedItem?.id; + const resolvedAgentId = agentId || window.VCPMainChatState?.snapshot?.()?.selectedItem?.id; const session = this.sessions.get(resolvedAgentId); agentId = resolvedAgentId; if (!session) { @@ -635,7 +635,7 @@ class FlowlockManager { * 切换 Agent 后也可无参数调用,以当前可见 Agent 的真实 Session 为准。 */ syncCurrentHeaderIndicator() { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; const header = document.getElementById('currentChatAgentName'); if (!header) return; @@ -651,7 +651,7 @@ class FlowlockManager { } updateCurrentHeaderIndicator(agentId, active) { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; if (currentItem?.type !== 'agent' || currentItem.id !== agentId) return; const header = document.getElementById('currentChatAgentName'); @@ -667,7 +667,7 @@ class FlowlockManager { } triggerCurrentHeaderHeartbeat(agentId) { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; if (currentItem?.type !== 'agent' || currentItem.id !== agentId) return; const header = document.getElementById('currentChatAgentName'); @@ -747,4 +747,4 @@ const flowlockManager = new FlowlockManager(); // 导出到window对象供其他模块使用 window.flowlockManager = flowlockManager; -console.log('[Flowlock] Manager instance created and exposed globally (multi-session).'); \ No newline at end of file +console.log('[Flowlock] Manager instance created and exposed globally (multi-session).'); diff --git a/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js b/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js index e99353423..95ad0444e 100644 --- a/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js +++ b/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js @@ -28,7 +28,7 @@ } function currentAgent() { - const item = window.currentSelectedItem; + const item = window.VCPMainChatState?.snapshot?.()?.selectedItem; return item?.type === 'agent' && item.id ? item : null; } diff --git a/Voicechatmodules/voicechat.html b/Voicechatmodules/voicechat.html index 80a5c315e..6f1de3daf 100644 --- a/Voicechatmodules/voicechat.html +++ b/Voicechatmodules/voicechat.html @@ -48,7 +48,6 @@

语音聊天

- - \ No newline at end of file + diff --git a/Voicechatmodules/voicechat.js b/Voicechatmodules/voicechat.js index 44a644c19..b67c6833d 100644 --- a/Voicechatmodules/voicechat.js +++ b/Voicechatmodules/voicechat.js @@ -1,6 +1,17 @@ -// Voicechatmodules/voicechat.js - -document.addEventListener('DOMContentLoaded', () => { +// Voicechatmodules/voicechat.js +import { createMemoryChatRepository } from '../modules/chat/memoryChatRepository.js'; +import { createTransientChatHistoryPersistence } from '../modules/chat/chatHistoryPersistence.js'; +import { createChatHistoryMutationAuthority } from '../modules/chat/chatHistoryMutationAuthority.js'; +import { createChatRepository } from '../modules/chat/chatRepository.js'; +import { createWindowStreamRuntime } from '../modules/renderer/windowStreamRuntime.js'; +import { createMessageRenderer } from '../modules/messageRenderer.js'; +import { createStreamProjection } from '../modules/renderer/streamManager.js'; +import { createStreamTransientHistory } from '../modules/chat/streamTransientHistory.js'; + +const streamManager = createStreamProjection(); +const messageRenderer = createMessageRenderer({ streamManager }); + +document.addEventListener('DOMContentLoaded', () => { const chatMessagesDiv = document.getElementById('chatMessages'); const messageInput = document.getElementById('messageInput'); const sendMessageBtn = document.getElementById('sendMessageBtn'); @@ -9,7 +20,8 @@ document.addEventListener('DOMContentLoaded', () => { const closeBtn = document.getElementById('close-btn-voicechat'); const toggleInputModeBtn = document.getElementById('toggleInputModeBtn'); const keyboardIcon = document.getElementById('keyboard-icon'); - const micIcon = document.getElementById('mic-icon'); + const micIcon = document.getElementById('mic-icon'); + let historyMutationAuthority = null; // Initialize audio context on first user gesture function initAudioContext() { @@ -51,7 +63,8 @@ document.addEventListener('DOMContentLoaded', () => { let agentId = null; let globalSettings = {}; let currentChatHistory = []; - let activeStreamingMessageId = null; + let activeStreamingMessageId = null; + let streamRuntime = null; let inputMode = 'text'; // 'text' or 'voice' const markedInstance = new window.marked.Marked({ gfm: true, breaks: true }); let speechRecognitionTimeout = null; @@ -71,13 +84,22 @@ document.addEventListener('DOMContentLoaded', () => { }; // --- Event Listeners --- - closeBtn.addEventListener('click', async () => { - closeBtn.disabled = true; - try { - await saveVoiceChatToHistory(); - } finally { - window.close(); - } + closeBtn.addEventListener('click', async () => { + closeBtn.disabled = true; + try { + if (activeStreamingMessageId) { + await window.electronAPI.interruptVcpRequest?.({ messageId: activeStreamingMessageId }); + await streamRuntime?.cancel(activeStreamingMessageId, 'voice-window-close'); + } + await saveVoiceChatToHistory(); + } finally { + await streamRuntime?.dispose(); + await streamManager.dispose(); + await messageRenderer.disposeRootResources(chatMessagesDiv); + messageRenderer.disposeRendererResources(); + await historyMutationAuthority?.dispose(); + window.close(); + } }); function getVoiceTopicId() { @@ -96,44 +118,17 @@ document.addEventListener('DOMContentLoaded', () => { return !!expectedTopicId && eventTopicId === expectedTopicId && eventAgentId === agentId; } - async function waitForActiveStreamToSettle(timeoutMs = 4000) { - if (!activeStreamingMessageId) return true; - - const pendingMessageId = activeStreamingMessageId; - console.log(`[VoiceChat] Waiting for active stream to settle before close: ${pendingMessageId}`); - - return await new Promise((resolve) => { - let settled = false; - const startedAt = Date.now(); - - const check = () => { - if (settled) return; - if (activeStreamingMessageId !== pendingMessageId) { - settled = true; - resolve(true); - return; - } - - if (Date.now() - startedAt >= timeoutMs) { - console.warn(`[VoiceChat] Timed out while waiting stream to settle: ${pendingMessageId}`); - settled = true; - resolve(false); - return; - } - - setTimeout(check, 80); - }; - - check(); - }); - } + function restoreComposerAfterStream() { + activeStreamingMessageId = null; + messageInput.disabled = false; + sendMessageBtn.disabled = false; + messageInput.focus(); + } async function saveVoiceChatToHistory() { if (!agentId) return; - await waitForActiveStreamToSettle(); - - const persistedHistory = currentChatHistory.filter(msg => !msg.isThinking && msg.role !== 'system'); + const persistedHistory = currentChatHistory.filter(msg => !msg.isThinking && msg.role !== 'system'); if (persistedHistory.length === 0) return; console.log('[VoiceChat] Saving chat history before exit...'); @@ -145,7 +140,8 @@ document.addEventListener('DOMContentLoaded', () => { if (result && result.success && result.topicId) { const newTopicId = result.topicId; - await window.electronAPI.saveChatHistory(agentId, newTopicId, persistedHistory); + if (!historyMutationAuthority) throw new Error('Voice history mutation authority is not ready'); + await historyMutationAuthority.replace({ itemId: agentId, itemType: 'agent', topicId: newTopicId, category: 'voice-session-close' }, persistedHistory); console.log(`[VoiceChat] History saved to new topic: ${newTopicId}`); if (window.summarizeTopicFromMessages) { @@ -266,7 +262,7 @@ document.addEventListener('DOMContentLoaded', () => { }); function initializeRenderer() { - if (window.messageRenderer) { + if (messageRenderer) { const chatHistoryRef = { get: () => currentChatHistory, set: (newHistory) => { currentChatHistory = newHistory; } @@ -289,21 +285,57 @@ document.addEventListener('DOMContentLoaded', () => { get: () => getVoiceTopicId(), set: () => {} }; - window.messageRenderer.initializeMessageRenderer({ - currentChatHistoryRef: chatHistoryRef, + const chatRepository = createMemoryChatRepository({ + read: () => currentChatHistory, + write: history => { currentChatHistory = history; }, + }); + const historyPersistence = createTransientChatHistoryPersistence(chatRepository); + const transientStreamHistory = createStreamTransientHistory({ + repository: chatRepository, + currentHistory: { get: () => chatHistoryRef.get(), replace: history => chatHistoryRef.set(history) }, + }); + historyMutationAuthority = createChatHistoryMutationAuthority({ repository: createChatRepository(window.electronAPI) }); + messageRenderer.initializeMessageRenderer({ + chatRepository, + historyMutationAuthority, + currentChatHistoryRef: chatHistoryRef, currentSelectedItemRef: selectedItemRef, - currentTopicIdRef: topicIdRef, + currentTopicIdRef: topicIdRef, + transientStreamHistory, + viewAuthority: { isCurrent: context => context?.agentId === agentId && context?.topicId === getVoiceTopicId() }, globalSettingsRef: globalSettingsRef, chatMessagesDiv: chatMessagesDiv, - electronAPI: window.electronAPI, - markedInstance: markedInstance, - uiHelper: uiHelperFunctions, // Pass the local helper - summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => ""), + electronAPI: window.electronAPI, + markedInstance: markedInstance, + morphdom: window.morphdom, + pretextBridge: window.pretextBridge, + flowlockProtocol: window.flowlockProtocol, + uiHelper: uiHelperFunctions, // Pass the local helper + messageCommands: { handleSendMessage: text => sendMessage(text) }, + summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => ""), handleCreateBranch: () => {} // Stub - }); - console.log('[VoiceChat] Shared messageRenderer initialized.'); + }); + streamRuntime = createWindowStreamRuntime({ + root: chatMessagesDiv, + streamProjection: streamManager, + historyPersistence, + getSelection: () => ({ id: agentId, type: 'agent' }), + getTopicId: getVoiceTopicId, + getMessageContext: () => ({ + agentId, topicId: getVoiceTopicId(), agentName: agentConfig?.name, + avatarUrl: agentConfig?.avatarUrl, avatarColor: agentConfig?.avatarCalculatedColor, + }), + contextFilter: context => !!context && context.topicId === getVoiceTopicId() && context.agentId === agentId, + dispatchTerminal: detail => window.dispatchEvent(new CustomEvent('vcp-chat-stream-terminal', { detail })), + afterPersist: ({ terminal, finalized }) => { + if (terminal.kind !== 'completed') return; + if (finalized?.messageId) setTimeout(() => extractTextAndPlayTTS(finalized.messageId, 0), 100); + }, + onSettled: restoreComposerAfterStream, + }); + console.log('[VoiceChat] Shared messageRenderer initialized.'); } else { - console.error('[VoiceChat] window.messageRenderer is not available.'); + console.error('[VoiceChat] shared message renderer provider is not available.'); } } @@ -327,10 +359,10 @@ document.addEventListener('DOMContentLoaded', () => { const sendMessage = async (messageContent) => { clearTimeout(speechRecognitionTimeout); // Stop any pending auto-send - if (!messageContent.trim() || !agentConfig || !window.messageRenderer) return; + if (!messageContent.trim() || !agentConfig || !messageRenderer) return; const userMessage = { role: 'user', content: messageContent, timestamp: Date.now(), id: `user_msg_${Date.now()}` }; - await window.messageRenderer.renderMessage(userMessage); + await messageRenderer.renderMessage(userMessage); currentChatHistory.push(userMessage); messageInput.value = ''; @@ -349,7 +381,7 @@ document.addEventListener('DOMContentLoaded', () => { name: agentConfig.name, avatarUrl: agentConfig.avatarUrl }; - await window.messageRenderer.renderMessage(assistantMessagePlaceholder); + await messageRenderer.renderMessage(assistantMessagePlaceholder); const context = { agentId: agentId, @@ -385,73 +417,20 @@ document.addEventListener('DOMContentLoaded', () => { await window.electronAPI.sendToVCP(globalSettings.vcpServerUrl, globalSettings.vcpApiKey, messagesForVCP, modelConfig, thinkingMessageId, false, context); - } catch (error) { - console.error('Error sending message to VCP:', error); - if (window.messageRenderer) { - window.messageRenderer.finalizeStreamedMessage(thinkingMessageId, 'error'); - const messageItemContent = document.querySelector(`.message-item[data-message-id="${thinkingMessageId}"] .md-content`); - if (messageItemContent) { - messageItemContent.innerHTML = `

请求失败: ${error.message}

`; - } - } - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - messageInput.focus(); - } - }; - - const activeStreams = new Set(); - window.electronAPI.onVCPStreamEvent((eventData) => { - if (!window.messageRenderer || !isEventForCurrentVoiceSession(eventData)) return; - - const { messageId, type, chunk, error, context } = eventData; - - if (!activeStreams.has(messageId) && type === 'data') { - window.messageRenderer.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: agentConfig.name, - avatarUrl: agentConfig.avatarUrl, - context: context, - }); - activeStreams.add(messageId); - } - - if (type === 'data') { - window.messageRenderer.appendStreamChunk(messageId, chunk, context); - } else if (type === 'end') { - console.log(`[VoiceChat] 收到流结束事件,messageId: ${messageId}`); - console.log(`[VoiceChat] 当前activeStreamingMessageId: ${activeStreamingMessageId}`); - console.log(`[VoiceChat] agentConfig状态: ${!!agentConfig}, TTS语音: ${agentConfig?.ttsVoicePrimary || '未设置'}`); - - window.messageRenderer.finalizeStreamedMessage(messageId, 'completed', context).then(() => { - console.log(`[VoiceChat] finalizeStreamedMessage完成,准备TTS`); - - // 添加延迟以确保DOM完全渲染 - setTimeout(() => { - extractTextAndPlayTTS(messageId, 0); - }, 100); - }); - - activeStreams.delete(messageId); - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - messageInput.focus(); - } else if (type === 'error') { - window.messageRenderer.finalizeStreamedMessage(messageId, 'error', context); - const messageItemContent = document.querySelector(`.message-item[data-message-id="${messageId}"] .md-content`); - if (messageItemContent) { - messageItemContent.innerHTML = `

${error || '未知流错误'}

`; - } - activeStreams.delete(messageId); - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - messageInput.focus(); - } - }); + } catch (error) { + console.error('Error sending message to VCP:', error); + const accepted = streamRuntime?.accept({ + type: 'error', messageId: thinkingMessageId, error: error.message, + context: { agentId, topicId: getVoiceTopicId() }, + }); + if (!accepted) restoreComposerAfterStream(); + } + }; + + window.electronAPI.onVCPStreamEvent((eventData) => { + if (!streamRuntime || !isEventForCurrentVoiceSession(eventData)) return; + streamRuntime.accept(eventData); + }); // 新增:智能文本提取和TTS触发函数,包含重试机制 function extractTextAndPlayTTS(messageId, retryCount = 0) { @@ -463,8 +442,8 @@ document.addEventListener('DOMContentLoaded', () => { if (messageElement) { const contentElement = messageElement.querySelector('.md-content'); - if (contentElement && window.messageRenderer?.extractSpeakableTextFromContentElement) { - textToSpeak = window.messageRenderer.extractSpeakableTextFromContentElement(contentElement); + if (contentElement && messageRenderer?.extractSpeakableTextFromContentElement) { + textToSpeak = messageRenderer.extractSpeakableTextFromContentElement(contentElement); } else if (contentElement) { const contentClone = contentElement.cloneNode(true); contentClone.querySelectorAll('.vcp-tool-use-bubble, .vcp-tool-result-bubble, .vcp-tool-call-summary-bubble, .maid-diary-bubble, .vcp-role-divider, .vcp-thought-chain-bubble, style, script').forEach(el => el.remove()); @@ -501,8 +480,8 @@ document.addEventListener('DOMContentLoaded', () => { console.log(`[VoiceChat] 找到备用匹配元素: ${idAttr}`); const contentElement = item.querySelector('.md-content'); if (contentElement) { - const backupText = window.messageRenderer?.extractSpeakableTextFromContentElement - ? window.messageRenderer.extractSpeakableTextFromContentElement(contentElement) + const backupText = messageRenderer?.extractSpeakableTextFromContentElement + ? messageRenderer.extractSpeakableTextFromContentElement(contentElement) : (contentElement.innerText || '').replace(/\n{3,}/g, '\n\n').trim(); if (backupText.trim().length > 0) { console.log(`[VoiceChat] 使用备用元素提取到文本长度: ${backupText.trim().length}`); @@ -674,4 +653,4 @@ document.addEventListener('DOMContentLoaded', () => { }, SPEECH_TIMEOUT_DURATION); } }); -}); \ No newline at end of file +}); diff --git a/artifacts/manual-soak/2026-08-20T20-28-29-513Z.json b/artifacts/manual-soak/2026-08-20T20-28-29-513Z.json new file mode 100644 index 000000000..84aa33516 --- /dev/null +++ b/artifacts/manual-soak/2026-08-20T20-28-29-513Z.json @@ -0,0 +1,144 @@ +{ + "schemaVersion": 1, + "status": "manual_observation_required", + "startedAt": "2026-08-20T20:28:29.513Z", + "endedAt": "2026-08-20T20:28:33.493Z", + "requestedMinutes": 0.02, + "intervalSeconds": 1, + "stopReason": "duration_elapsed", + "host": { + "platform": "win32", + "arch": "x64", + "hostname": "FlowX13", + "node": "v22.17.1" + }, + "checklist": [ + { + "item": "main chat send/stream/cancel/retry", + "observed": null, + "notes": "" + }, + { + "item": "history and topic switching", + "observed": null, + "notes": "" + }, + { + "item": "attachments", + "observed": null, + "notes": "" + }, + { + "item": "theme", + "observed": null, + "notes": "" + }, + { + "item": "notifications and desktop push", + "observed": null, + "notes": "" + }, + { + "item": "VoiceChat", + "observed": null, + "notes": "" + }, + { + "item": "Rust Assistant", + "observed": null, + "notes": "" + }, + { + "item": "renderer reload/crash recovery", + "observed": null, + "notes": "" + }, + { + "item": "Classic child pages and plugin protocol", + "observed": null, + "notes": "" + }, + { + "item": "observe detached roots/listeners/heap/errors throughout the session", + "observed": null, + "notes": "" + } + ], + "checkpoints": [ + { + "label": "baseline", + "at": "2026-08-20T20:28:32.097Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1132, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 10670540, + "heapTotal": 14290944, + "rendererProcesses": 2, + "errors": [] + }, + { + "label": "sample-1", + "at": "2026-08-20T20:28:33.115Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1185, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 9528524, + "heapTotal": 14815232, + "rendererProcesses": 2, + "errors": [] + }, + { + "label": "sample-2", + "at": "2026-08-20T20:28:33.316Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1185, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 9547680, + "heapTotal": 14815232, + "rendererProcesses": 2, + "errors": [] + }, + { + "label": "final", + "at": "2026-08-20T20:28:33.321Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1185, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 9559864, + "heapTotal": 14815232, + "rendererProcesses": 2, + "errors": [] + } + ], + "electronStderrTail": "\r\nDevTools listening on ws://127.0.0.1:51214/devtools/browser/97f0314d-0890-4c03-a1c3-82f261fa23b5\r\n[Main] Failed to fetch and cache models: [TypeError: fetch failed] {\n [cause]: Error: bad port\n at makeNetworkError (node:internal/deps/undici/undici:11790:35)\n at mainFetch (node:internal/deps/undici/undici:13173:22)\n at fetching (node:internal/deps/undici/undici:13160:7)\n at fetch (node:internal/deps/undici/undici:13024:20)\n at fetch (node:internal/deps/undici/undici:17692:10)\n at fetch (node:internal/bootstrap/web/exposed-window-or-worker:83:12)\n at fetchAndCacheModels (C:\\VCP\\vchat-develop\\VCPChat-design-system-no-workflow-windows-test\\main.js:937:40)\n}\n[Music] ========== INITIALIZE CALLED ==========\n[Music] About to call registerIpcHandlers...\n[Music] IPC handlers registered.\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n[Main] Failed to register global shortcut: Super+Alt+Z\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n", + "errors": [], + "operatorNote": "This artifact is an observation log, not a pass result. A human must review every checklist item and record pass/fail separately." +} diff --git a/artifacts/manual-soak/2026-08-20T20-58-24-947Z.json b/artifacts/manual-soak/2026-08-20T20-58-24-947Z.json new file mode 100644 index 000000000..88de019b7 --- /dev/null +++ b/artifacts/manual-soak/2026-08-20T20-58-24-947Z.json @@ -0,0 +1,126 @@ +{ + "schemaVersion": 1, + "status": "manual_observation_required", + "startedAt": "2026-08-20T20:58:24.947Z", + "endedAt": "2026-08-20T20:58:28.997Z", + "requestedMinutes": 0.01, + "intervalSeconds": 1, + "stopReason": "duration_elapsed", + "host": { + "platform": "win32", + "arch": "x64", + "hostname": "FlowX13", + "node": "v22.17.1" + }, + "checklist": [ + { + "item": "main chat send/stream/cancel/retry", + "observed": null, + "notes": "" + }, + { + "item": "history and topic switching", + "observed": null, + "notes": "" + }, + { + "item": "attachments", + "observed": null, + "notes": "" + }, + { + "item": "theme", + "observed": null, + "notes": "" + }, + { + "item": "notifications and desktop push", + "observed": null, + "notes": "" + }, + { + "item": "VoiceChat", + "observed": null, + "notes": "" + }, + { + "item": "Rust Assistant", + "observed": null, + "notes": "" + }, + { + "item": "renderer reload/crash recovery", + "observed": null, + "notes": "" + }, + { + "item": "Classic child pages and plugin protocol", + "observed": null, + "notes": "" + }, + { + "item": "observe detached roots/listeners/heap/errors throughout the session", + "observed": null, + "notes": "" + } + ], + "checkpoints": [ + { + "label": "baseline", + "at": "2026-08-20T20:58:27.527Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1132, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 10765644, + "heapTotal": 14815232, + "rendererProcesses": 2, + "errors": [] + }, + { + "label": "sample-1", + "at": "2026-08-20T20:58:28.157Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1185, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 11436444, + "heapTotal": 15077376, + "rendererProcesses": 2, + "errors": [] + }, + { + "label": "final", + "at": "2026-08-20T20:58:28.186Z", + "state": { + "ready": "true", + "uiMode": "next", + "connectedElements": 1185, + "listeners": null, + "lifecycleActiveResources": 79, + "lifecycleActiveScopes": 8, + "detachedRoots": 0, + "bodyText": 249 + }, + "heapUsed": 11453928, + "heapTotal": 15077376, + "rendererProcesses": 2, + "errors": [] + } + ], + "electronStderrTail": "\r\nDevTools listening on ws://127.0.0.1:52314/devtools/browser/7525cf39-7b0c-4fbf-86e4-cfe470dd412c\r\n[Main] Failed to fetch and cache models: [TypeError: fetch failed] {\n [cause]: Error: bad port\n at makeNetworkError (node:internal/deps/undici/undici:11790:35)\n at mainFetch (node:internal/deps/undici/undici:13173:22)\n at fetching (node:internal/deps/undici/undici:13160:7)\n at fetch (node:internal/deps/undici/undici:13024:20)\n at fetch (node:internal/deps/undici/undici:17692:10)\n at fetch (node:internal/bootstrap/web/exposed-window-or-worker:83:12)\n at fetchAndCacheModels (C:\\VCP\\vchat-develop\\VCPChat-design-system-no-workflow-windows-test\\main.js:937:40)\n}\n[Music] ========== INITIALIZE CALLED ==========\n[Music] About to call registerIpcHandlers...\n[Music] IPC handlers registered.\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n[Main] Failed to register global shortcut: Super+Alt+Z\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n[EmoticonFixer] Emoji library degraded: missing fileKey in settings.json\n", + "errors": [], + "operatorNote": "This artifact is an observation log, not a pass result. A human must review every checklist item and record pass/fail separately." +} diff --git a/artifacts/windows-matrix/2026-08-20T20-12-51-026Z.json b/artifacts/windows-matrix/2026-08-20T20-12-51-026Z.json new file mode 100644 index 000000000..d0089cf55 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-20T20-12-51-026Z.json @@ -0,0 +1,21 @@ +{ + "generatedAt": "2026-08-20T20:12:51.028Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe node_modules\\npm\\bin\\npm-cli.js run test:electron-ui-apps", + "env": {}, + "startedAt": "2026-08-20T20:12:51.040Z", + "finishedAt": "2026-08-20T20:12:51.167Z", + "exitCode": 1, + "signal": null, + "status": "failed", + "output": "node:internal/modules/cjs/loader:1404\r\n throw err;\r\n ^\r\n\r\nError: Cannot find module 'C:\\VCP\\vchat-develop\\VCPChat-design-system-no-workflow-windows-test\\node_modules\\npm\\bin\\npm-cli.js'\r\n at Function._resolveFilename (node:internal/modules/cjs/loader:1401:15)\r\n at defaultResolveImpl (node:internal/modules/cjs/loader:1057:19)\r\n at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1062:22)\r\n at Function._load (node:internal/modules/cjs/loader:1211:37)\r\n at TracingChannel.traceSync (node:diagnostics_channel:322:14)\r\n at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\r\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)\r\n at node:internal/main/run_main_module:36:49 {\r\n code: 'MODULE_NOT_FOUND',\r\n requireStack: []\r\n}\r\n\r\nNode.js v22.17.1\r\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-20T20-13-18-584Z.json b/artifacts/windows-matrix/2026-08-20T20-13-18-584Z.json new file mode 100644 index 000000000..93393dca7 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-20T20-13-18-584Z.json @@ -0,0 +1,32 @@ +{ + "generatedAt": "2026-08-20T20:13:18.586Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-20T20:13:18.596Z", + "finishedAt": "2026-08-20T20:13:54.309Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-20T20:13:54.310Z", + "finishedAt": "2026-08-20T20:15:33.191Z", + "exitCode": 1, + "signal": null, + "status": "failed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, in-memory settings updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\nSettings WA Electron gate failed:\nTimeoutError: Waiting failed: 90000ms exceeded\n at new WaitTask (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/WaitTask.js:45:34)\n at IsolatedWorld.waitForFunction (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/Realm.js:22:26)\n at CdpFrame.waitForFunction (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/Frame.js:584:43)\n at CdpFrame. (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/util/decorators.js:101:27)\n at CdpPage.waitForFunction (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/Page.js:1439:37)\n at file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/scripts/test-settings-wa-electron.mjs:292:16\n\nRenderer errors:\n- console.error: Failed to load resource: net::ERR_FILE_NOT_FOUND\n- console.error: Failed to load resource: net::ERR_FILE_NOT_FOUND\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-20T20-16-47-662Z.json b/artifacts/windows-matrix/2026-08-20T20-16-47-662Z.json new file mode 100644 index 000000000..565ec5cd4 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-20T20-16-47-662Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-20T20:16:47.664Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-20T20:16:47.674Z", + "finishedAt": "2026-08-20T20:17:23.278Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-20T20:17:23.278Z", + "finishedAt": "2026-08-20T20:17:32.125Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, in-memory settings updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-20T20:17:32.125Z", + "finishedAt": "2026-08-20T20:17:34.196Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-20T20:17:34.197Z", + "finishedAt": "2026-08-20T20:17:59.489Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-20T20:17:59.490Z", + "finishedAt": "2026-08-20T20:18:30.134Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-20T20:18:30.135Z", + "finishedAt": "2026-08-20T20:22:17.657Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10818676,\"jsHeapUsed\":10818676,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10615060,\"jsHeapUsed\":10615060,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10712540,\"jsHeapUsed\":10712540,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10730096,\"jsHeapUsed\":10730096,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10746704,\"jsHeapUsed\":10746704,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10764848,\"jsHeapUsed\":10764848,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10808088,\"jsHeapUsed\":10808088,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":10825712,\"jsHeapUsed\":10825712,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":10843520,\"jsHeapUsed\":10843520,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":10851980,\"jsHeapUsed\":10851980,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":10858716,\"jsHeapUsed\":10858716,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":10867088,\"jsHeapUsed\":10867088,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":10871024,\"jsHeapUsed\":10871024,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.1 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-20T20-49-30-813Z.json b/artifacts/windows-matrix/2026-08-20T20-49-30-813Z.json new file mode 100644 index 000000000..dfcf2f62c --- /dev/null +++ b/artifacts/windows-matrix/2026-08-20T20-49-30-813Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-20T20:49:30.815Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-20T20:49:30.828Z", + "finishedAt": "2026-08-20T20:50:06.301Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-20T20:50:06.302Z", + "finishedAt": "2026-08-20T20:50:15.164Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, in-memory settings updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-20T20:50:15.164Z", + "finishedAt": "2026-08-20T20:50:17.211Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-20T20:50:17.212Z", + "finishedAt": "2026-08-20T20:50:42.735Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-20T20:50:42.736Z", + "finishedAt": "2026-08-20T20:51:13.435Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-20T20:51:13.436Z", + "finishedAt": "2026-08-20T20:55:02.293Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10833232,\"jsHeapUsed\":10833232,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10695040,\"jsHeapUsed\":10695040,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10727304,\"jsHeapUsed\":10727304,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10745404,\"jsHeapUsed\":10745404,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10761668,\"jsHeapUsed\":10761668,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10779860,\"jsHeapUsed\":10779860,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10817352,\"jsHeapUsed\":10817352,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":10841032,\"jsHeapUsed\":10841032,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":10857960,\"jsHeapUsed\":10857960,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":10867012,\"jsHeapUsed\":10867012,\"documents\":3,\"nodes\":10727,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":10874908,\"jsHeapUsed\":10874908,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":10882976,\"jsHeapUsed\":10882976,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":10886584,\"jsHeapUsed\":10886584,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.4 MiB' │ 10727 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-20T20-59-10-746Z.json b/artifacts/windows-matrix/2026-08-20T20-59-10-746Z.json new file mode 100644 index 000000000..177f1baf7 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-20T20-59-10-746Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-20T20:59:10.748Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-20T20:59:10.760Z", + "finishedAt": "2026-08-20T20:59:46.083Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-20T20:59:46.083Z", + "finishedAt": "2026-08-20T20:59:54.886Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, in-memory settings updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-20T20:59:54.886Z", + "finishedAt": "2026-08-20T20:59:56.905Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-20T20:59:56.906Z", + "finishedAt": "2026-08-20T21:00:22.501Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-20T21:00:22.502Z", + "finishedAt": "2026-08-20T21:00:52.715Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-20T21:00:52.715Z", + "finishedAt": "2026-08-20T21:04:39.953Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10835592,\"jsHeapUsed\":10835592,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10630820,\"jsHeapUsed\":10630820,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10728092,\"jsHeapUsed\":10728092,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10746464,\"jsHeapUsed\":10746464,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10761468,\"jsHeapUsed\":10761468,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10778692,\"jsHeapUsed\":10778692,\"documents\":3,\"nodes\":10730,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10812752,\"jsHeapUsed\":10812752,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":10828152,\"jsHeapUsed\":10828152,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":10844464,\"jsHeapUsed\":10844464,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":10853412,\"jsHeapUsed\":10853412,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":10859700,\"jsHeapUsed\":10859700,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":10882468,\"jsHeapUsed\":10882468,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":10886940,\"jsHeapUsed\":10886940,\"documents\":3,\"nodes\":10718,\"listeners\":875,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.1 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.2 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.3 MiB' │ 10730 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.3 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.4 MiB' │ 10718 │ 2569 │ 875 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T04-47-10-737Z.json b/artifacts/windows-matrix/2026-08-21T04-47-10-737Z.json new file mode 100644 index 000000000..2c40b24c4 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T04-47-10-737Z.json @@ -0,0 +1,32 @@ +{ + "generatedAt": "2026-08-21T04:47:10.739Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T04:47:10.752Z", + "finishedAt": "2026-08-21T04:47:45.263Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T04:47:45.263Z", + "finishedAt": "2026-08-21T04:47:52.771Z", + "exitCode": 1, + "signal": null, + "status": "failed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\nSettings WA Electron gate failed:\nAssertionError [ERR_ASSERTION]: globalSettings reflects the persisted value after save\n+ actual - expected\n\n+ undefined\n- '请继续-电子-1787287671967'\n\n at file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/scripts/test-settings-wa-electron.mjs:269:12\n\nRenderer errors:\n- console.error: Failed to load resource: net::ERR_FILE_NOT_FOUND\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T04-49-00-860Z.json b/artifacts/windows-matrix/2026-08-21T04-49-00-860Z.json new file mode 100644 index 000000000..514922afb --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T04-49-00-860Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-21T04:49:00.862Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T04:49:00.871Z", + "finishedAt": "2026-08-21T04:49:35.637Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T04:49:35.637Z", + "finishedAt": "2026-08-21T04:49:44.229Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T04:49:44.230Z", + "finishedAt": "2026-08-21T04:49:46.223Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T04:49:46.223Z", + "finishedAt": "2026-08-21T04:50:11.520Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-21T04:50:11.521Z", + "finishedAt": "2026-08-21T04:50:42.587Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-21T04:50:42.587Z", + "finishedAt": "2026-08-21T05:00:28.899Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10833576,\"jsHeapUsed\":10833576,\"documents\":3,\"nodes\":11072,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10654352,\"jsHeapUsed\":10654352,\"documents\":3,\"nodes\":11907,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10778032,\"jsHeapUsed\":10778032,\"documents\":3,\"nodes\":12831,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10815572,\"jsHeapUsed\":10815572,\"documents\":3,\"nodes\":13656,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10861784,\"jsHeapUsed\":10861784,\"documents\":3,\"nodes\":14582,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10907008,\"jsHeapUsed\":10907008,\"documents\":3,\"nodes\":15419,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10971044,\"jsHeapUsed\":10971044,\"documents\":3,\"nodes\":16348,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":11008088,\"jsHeapUsed\":11008088,\"documents\":3,\"nodes\":17170,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":11049120,\"jsHeapUsed\":11049120,\"documents\":3,\"nodes\":18096,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":11081340,\"jsHeapUsed\":11081340,\"documents\":3,\"nodes\":18933,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":11114032,\"jsHeapUsed\":11114032,\"documents\":3,\"nodes\":19857,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":11151028,\"jsHeapUsed\":11151028,\"documents\":3,\"nodes\":20682,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":11183696,\"jsHeapUsed\":11183696,\"documents\":3,\"nodes\":21608,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 11072 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.2 MiB' │ 11907 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.3 MiB' │ 12831 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.3 MiB' │ 13656 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.4 MiB' │ 14582 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.4 MiB' │ 15419 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.5 MiB' │ 16348 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.5 MiB' │ 17170 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.5 MiB' │ 18096 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.6 MiB' │ 18933 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.6 MiB' │ 19857 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.6 MiB' │ 20682 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.7 MiB' │ 21608 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T05-10-03-018Z.json b/artifacts/windows-matrix/2026-08-21T05-10-03-018Z.json new file mode 100644 index 000000000..8694052c0 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T05-10-03-018Z.json @@ -0,0 +1,54 @@ +{ + "generatedAt": "2026-08-21T05:10:03.019Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T05:10:03.029Z", + "finishedAt": "2026-08-21T05:10:38.010Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T05:10:38.010Z", + "finishedAt": "2026-08-21T05:10:46.726Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T05:10:46.726Z", + "finishedAt": "2026-08-21T05:10:48.464Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T05:10:48.465Z", + "finishedAt": "2026-08-21T05:14:26.560Z", + "exitCode": 1, + "signal": null, + "status": "failed", + "output": "file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:98\r\n #error = new ProtocolError();\r\n ^\r\n\r\nProtocolError: Runtime.callFunctionOn timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.\r\n at (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:98:14)\r\n at new Callback (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:102:16)\r\n at CallbackRegistry.create (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:21:26)\r\n at Connection._rawSend (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/Connection.js:95:26)\r\n at CdpCDPSession.send (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/CdpSession.js:70:33)\r\n at #evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:360:50)\r\n at ExecutionContext.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:274:36)\r\n at IsolatedWorld.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/IsolatedWorld.js:97:30)\r\n at CdpJSHandle.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/JSHandle.js:146:37)\r\n at CdpElementHandle.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/ElementHandle.js:340:38)\r\n\r\nNode.js v22.17.1\r\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T05-15-10-766Z.json b/artifacts/windows-matrix/2026-08-21T05-15-10-766Z.json new file mode 100644 index 000000000..ac2cb9e5f --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T05-15-10-766Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-21T05:15:10.768Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T05:15:10.780Z", + "finishedAt": "2026-08-21T05:15:45.225Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T05:15:45.225Z", + "finishedAt": "2026-08-21T05:15:53.698Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T05:15:53.698Z", + "finishedAt": "2026-08-21T05:15:55.721Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T05:15:55.722Z", + "finishedAt": "2026-08-21T05:16:21.226Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-21T05:16:21.227Z", + "finishedAt": "2026-08-21T05:16:52.955Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-21T05:16:52.956Z", + "finishedAt": "2026-08-21T05:26:39.505Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10844228,\"jsHeapUsed\":10844228,\"documents\":3,\"nodes\":11072,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10664676,\"jsHeapUsed\":10664676,\"documents\":3,\"nodes\":11907,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10788896,\"jsHeapUsed\":10788896,\"documents\":3,\"nodes\":12831,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10826920,\"jsHeapUsed\":10826920,\"documents\":3,\"nodes\":13656,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10871884,\"jsHeapUsed\":10871884,\"documents\":3,\"nodes\":14582,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10917364,\"jsHeapUsed\":10917364,\"documents\":3,\"nodes\":15419,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10981344,\"jsHeapUsed\":10981344,\"documents\":3,\"nodes\":16343,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":11018356,\"jsHeapUsed\":11018356,\"documents\":3,\"nodes\":17168,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":11059484,\"jsHeapUsed\":11059484,\"documents\":3,\"nodes\":18094,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":11091764,\"jsHeapUsed\":11091764,\"documents\":3,\"nodes\":18931,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":11124604,\"jsHeapUsed\":11124604,\"documents\":3,\"nodes\":19855,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":11160132,\"jsHeapUsed\":11160132,\"documents\":3,\"nodes\":20680,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":11194076,\"jsHeapUsed\":11194076,\"documents\":3,\"nodes\":21608,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 11072 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.2 MiB' │ 11907 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.3 MiB' │ 12831 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.3 MiB' │ 13656 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.4 MiB' │ 14582 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.4 MiB' │ 15419 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.5 MiB' │ 16343 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.5 MiB' │ 17168 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.5 MiB' │ 18094 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.6 MiB' │ 18931 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.6 MiB' │ 19855 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.6 MiB' │ 20680 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.7 MiB' │ 21608 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T05-51-24-115Z.json b/artifacts/windows-matrix/2026-08-21T05-51-24-115Z.json new file mode 100644 index 000000000..c7ed80374 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T05-51-24-115Z.json @@ -0,0 +1,54 @@ +{ + "generatedAt": "2026-08-21T05:51:24.116Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T05:51:24.126Z", + "finishedAt": "2026-08-21T05:51:57.215Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T05:51:57.216Z", + "finishedAt": "2026-08-21T05:52:05.350Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T05:52:05.351Z", + "finishedAt": "2026-08-21T05:52:07.126Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T05:52:07.126Z", + "finishedAt": "2026-08-21T05:55:22.760Z", + "exitCode": 1, + "signal": null, + "status": "failed", + "output": "file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:98\r\n #error = new ProtocolError();\r\n ^\r\n\r\nProtocolError: Runtime.callFunctionOn timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.\r\n at (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:98:14)\r\n at new Callback (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:102:16)\r\n at CallbackRegistry.create (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:21:26)\r\n at Connection._rawSend (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/Connection.js:95:26)\r\n at CdpCDPSession.send (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/CdpSession.js:70:33)\r\n at #evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:360:50)\r\n at ExecutionContext.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:274:36)\r\n at IsolatedWorld.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/IsolatedWorld.js:97:30)\r\n at CdpJSHandle.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/JSHandle.js:146:37)\r\n at CdpElementHandle.evaluate (file:///C:/VCP/vchat-develop/VCPChat-design-system-no-workflow-windows-test/node_modules/puppeteer-core/lib/esm/puppeteer/api/ElementHandle.js:340:38)\r\n\r\nNode.js v22.17.1\r\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T05-55-55-233Z.json b/artifacts/windows-matrix/2026-08-21T05-55-55-233Z.json new file mode 100644 index 000000000..b1d23770e --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T05-55-55-233Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-21T05:55:55.236Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T05:55:55.246Z", + "finishedAt": "2026-08-21T05:56:27.995Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T05:56:27.995Z", + "finishedAt": "2026-08-21T05:56:36.071Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T05:56:36.072Z", + "finishedAt": "2026-08-21T05:56:37.856Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T05:56:37.856Z", + "finishedAt": "2026-08-21T05:57:01.819Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-21T05:57:01.819Z", + "finishedAt": "2026-08-21T05:57:30.960Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-21T05:57:30.961Z", + "finishedAt": "2026-08-21T06:07:19.984Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10829872,\"jsHeapUsed\":10829872,\"documents\":3,\"nodes\":11072,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10649544,\"jsHeapUsed\":10649544,\"documents\":3,\"nodes\":11907,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10774552,\"jsHeapUsed\":10774552,\"documents\":3,\"nodes\":12831,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10811776,\"jsHeapUsed\":10811776,\"documents\":3,\"nodes\":13656,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10856740,\"jsHeapUsed\":10856740,\"documents\":3,\"nodes\":14582,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10901948,\"jsHeapUsed\":10901948,\"documents\":3,\"nodes\":15419,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10965920,\"jsHeapUsed\":10965920,\"documents\":3,\"nodes\":16347,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":11002352,\"jsHeapUsed\":11002352,\"documents\":3,\"nodes\":17172,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":11044016,\"jsHeapUsed\":11044016,\"documents\":3,\"nodes\":18098,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":11076184,\"jsHeapUsed\":11076184,\"documents\":3,\"nodes\":18935,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":11108928,\"jsHeapUsed\":11108928,\"documents\":3,\"nodes\":19859,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":11145924,\"jsHeapUsed\":11145924,\"documents\":3,\"nodes\":20684,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":11179832,\"jsHeapUsed\":11179832,\"documents\":3,\"nodes\":21613,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 11072 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.2 MiB' │ 11907 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.3 MiB' │ 12831 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.3 MiB' │ 13656 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.4 MiB' │ 14582 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.4 MiB' │ 15419 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.5 MiB' │ 16347 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.5 MiB' │ 17172 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.5 MiB' │ 18098 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.6 MiB' │ 18935 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.6 MiB' │ 19859 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.6 MiB' │ 20684 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.7 MiB' │ 21613 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json b/artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json new file mode 100644 index 000000000..8f69b8038 --- /dev/null +++ b/artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json @@ -0,0 +1,80 @@ +{ + "generatedAt": "2026-08-21T06:13:58.842Z", + "platform": "win32", + "arch": "x64", + "node": "v22.17.1", + "hostname": "FlowX13", + "matrixScope": "windows-current-host", + "rows": [ + { + "id": "ui-apps-next-classic", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-ui-apps-smoke.mjs", + "env": {}, + "startedAt": "2026-08-21T06:13:58.851Z", + "finishedAt": "2026-08-21T06:14:31.171Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "[electron-ui-apps] opening 便签\n[electron-ui-apps] reopening 便签 cycle 1\n[electron-ui-apps] reopening 便签 cycle 2\n[electron-ui-apps] reopening 便签 cycle 3\n[electron-ui-apps] reopening 便签 cycle 4\n[electron-ui-apps] opening 翻译\n[electron-ui-apps] opening 日志\n[electron-ui-apps] opening 插件\n[electron-ui-apps] opening 任务\n[electron-ui-apps] opening 笔记\n[electron-ui-apps] opening 记忆\n[electron-ui-apps] opening 论坛\nElectron UI apps smoke passed (canonical main layout plus upstream-Classic child host integration).\n\nUI apps audit summary (24/24 passed):\n [next] 主界面 shell: PASS — boot: WA 零请求/零注册,Orbitron/Nova/lucide 已载入,上游消息组件语义保留,应用托盘与 Ask Nova 可用\n [next] UI 组件库: PASS — lazy-registers WA;feedback owner 关闭后不影响主 Surface\n [next-readonly] 聊天历史: PASS — 独立 root、只读合同、关闭后 teardown\n [next-interactive] 独立聊天: PASS — 输入/发送/取消/错误状态合同与 teardown\n [next] 创建助手 Modal: PASS — WA 单一实现覆盖冷启动/主题/故障提示,提交和关闭生命周期可控\n [next] 全局设置: PASS — 增强输入/保存栏 dirty 态/搜索注入/焦点\n [next] Agent 设置生命周期: PASS — 20 次往返不增加 WebContents、VCPUI adapter 或提示词 DOM\n [upstream-classic] 便签: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 翻译: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 日志: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 插件: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 任务: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 笔记: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 记忆: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [upstream-classic] 论坛: PASS — 通用宿主打开上游经典页面,未携带实验性 Next 实现\n [classic] 便签: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 翻译: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 日志: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 插件: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 任务: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 笔记: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 记忆: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [classic] 论坛: PASS — 旧 DOM 保留,next 表面未挂载,无 WA\n [canonical] 主窗口与全局设置: PASS — 旧 Classic 配置无法拆卸唯一布局,共享输入、通知、壁纸与设置保持可用\n" + }, + { + "id": "settings-next", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-settings-wa-electron.mjs", + "env": {}, + "startedAt": "2026-08-21T06:14:31.172Z", + "finishedAt": "2026-08-21T06:14:39.094Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": " [PASS] 1. SettingsShell layout (nav list, search, save bar, sections, icons)\n [PASS] 2. category switching keeps unsaved values\n [PASS] 3. search locates and activates the matching category\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 4. dark screenshot -> screenshots/settings-wa-dark-700x500.png\n [PASS] 5. light screenshot -> screenshots/settings-wa-light-700x500.png (bg rgb(255, 255, 255))\n[test-settings-wa-electron] window resize skipped: Protocol error (Browser.getWindowForTarget): 'Browser.getWindowForTarget' wasn't found\n [PASS] 6. real save via IPC persists (modal closed, authority projection updated)\n [PASS] 7. reopen after reload restores persisted values from settings.json\n [PASS] 8. canonical Next SettingsShell survives reload\n\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).\n" + }, + { + "id": "next-tab-lifecycle", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-next-ui-tab-lifecycle.mjs", + "env": {}, + "startedAt": "2026-08-21T06:14:39.095Z", + "finishedAt": "2026-08-21T06:14:40.762Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Next UI tab lifecycle checks passed.\n" + }, + { + "id": "main-chat-default", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": {}, + "startedAt": "2026-08-21T06:14:40.762Z", + "finishedAt": "2026-08-21T06:15:04.253Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=25\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "auxiliary-crash-recovery", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-main-chat-sequences.mjs", + "env": { + "VCPCHAT_AUX_CRASH_MATRIX": "1" + }, + "startedAt": "2026-08-21T06:15:04.254Z", + "finishedAt": "2026-08-21T06:15:33.568Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Main-chat Electron sequence passed: mode=next, seed=next-main-chat-default, runs=1, actions=24, VCP requests=27\nSequence coverage: actions=11, pairs=21, transitions=12, faults=2, required=1/1\n" + }, + { + "id": "lifecycle-stress-60", + "command": "C:\\Program Files\\nodejs\\node.exe scripts\\test-electron-lifecycle-stress.mjs", + "env": { + "VCPCHAT_STRESS_CYCLES": "60" + }, + "startedAt": "2026-08-21T06:15:33.569Z", + "finishedAt": "2026-08-21T06:25:23.479Z", + "exitCode": 0, + "signal": null, + "status": "passed", + "output": "Lifecycle stress baseline: {\"label\":\"baseline\",\"heapUsed\":10829240,\"jsHeapUsed\":10829240,\"documents\":3,\"nodes\":11072,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-5\",\"heapUsed\":10649468,\"jsHeapUsed\":10649468,\"documents\":3,\"nodes\":11907,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-10\",\"heapUsed\":10772940,\"jsHeapUsed\":10772940,\"documents\":3,\"nodes\":12831,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-15\",\"heapUsed\":10810268,\"jsHeapUsed\":10810268,\"documents\":3,\"nodes\":13656,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-20\",\"heapUsed\":10855448,\"jsHeapUsed\":10855448,\"documents\":3,\"nodes\":14582,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-25\",\"heapUsed\":10900672,\"jsHeapUsed\":10900672,\"documents\":3,\"nodes\":15419,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-30\",\"heapUsed\":10964676,\"jsHeapUsed\":10964676,\"documents\":3,\"nodes\":16343,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-35\",\"heapUsed\":11001784,\"jsHeapUsed\":11001784,\"documents\":3,\"nodes\":17168,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-40\",\"heapUsed\":11042816,\"jsHeapUsed\":11042816,\"documents\":3,\"nodes\":18094,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-45\",\"heapUsed\":11075036,\"jsHeapUsed\":11075036,\"documents\":3,\"nodes\":18931,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-50\",\"heapUsed\":11107744,\"jsHeapUsed\":11107744,\"documents\":3,\"nodes\":19855,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-55\",\"heapUsed\":11144756,\"jsHeapUsed\":11144756,\"documents\":3,\"nodes\":20680,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nLifecycle stress checkpoint: {\"label\":\"cycle-60\",\"heapUsed\":11177460,\"jsHeapUsed\":11177460,\"documents\":3,\"nodes\":21606,\"listeners\":876,\"pages\":2,\"processes\":5,\"rendererProcesses\":2,\"detachedRoots\":0,\"detachedVcpIcons\":0,\"detachedOptions\":0,\"detachedSignatures\":[],\"detachedKinds\":{},\"lifecycleActiveScopes\":8,\"lifecycleActiveResources\":163,\"lifecycleResourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1},\"connectedElements\":2569,\"enhancedSettingsControls\":77,\"promptNodes\":31,\"lifecycle\":{\"activeScopes\":8,\"activeResources\":163,\"resourcesByType\":{\"listener\":65,\"dom-state\":4,\"child-scope\":3,\"ui-registration\":80,\"subscription\":3,\"controller\":7,\"observer\":1}},\"transientLifecycleScopes\":[],\"staleWeakNodes\":[]}\nElectron lifecycle stress passed (3 warmup + 60 measured cycles).\n┌─────────┬────────────┬────────────┬───────┬───────────┬───────────┬───────┬───────────┬──────────┬─────────────┬───────────────┬───────────────┬─────────────────┬─────────────────┬────────────────────┐\n│ (index) │ checkpoint │ heap │ nodes │ connected │ listeners │ pages │ processes │ enhanced │ promptNodes │ detachedRoots │ detachedIcons │ detachedOptions │ lifecycleScopes │ lifecycleResources │\n├─────────┼────────────┼────────────┼───────┼───────────┼───────────┼───────┼───────────┼──────────┼─────────────┼───────────────┼───────────────┼─────────────────┼─────────────────┼────────────────────┤\n│ 0 │ 'baseline' │ '10.3 MiB' │ 11072 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 1 │ 'cycle-5' │ '10.2 MiB' │ 11907 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 2 │ 'cycle-10' │ '10.3 MiB' │ 12831 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 3 │ 'cycle-15' │ '10.3 MiB' │ 13656 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 4 │ 'cycle-20' │ '10.4 MiB' │ 14582 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 5 │ 'cycle-25' │ '10.4 MiB' │ 15419 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 6 │ 'cycle-30' │ '10.5 MiB' │ 16343 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 7 │ 'cycle-35' │ '10.5 MiB' │ 17168 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 8 │ 'cycle-40' │ '10.5 MiB' │ 18094 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 9 │ 'cycle-45' │ '10.6 MiB' │ 18931 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 10 │ 'cycle-50' │ '10.6 MiB' │ 19855 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 11 │ 'cycle-55' │ '10.6 MiB' │ 20680 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n│ 12 │ 'cycle-60' │ '10.7 MiB' │ 21606 │ 2569 │ 876 │ 2 │ 5 │ 77 │ 31 │ 0 │ 0 │ 0 │ 8 │ 163 │\n└─────────┴────────────┴────────────┴───────┴───────────┴───────────┴───────┴───────────┴──────────┴─────────────┴───────────────┴───────────────┴─────────────────┴─────────────────┴────────────────────┘\n" + } + ] +} \ No newline at end of file diff --git a/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/README.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/README.md new file mode 100644 index 000000000..414339415 --- /dev/null +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/README.md @@ -0,0 +1,18 @@ +# 2026-08 Chat Kernel 与 UI 路线归档 + +此目录保存已经完成、停止执行或按时间追加的开发路线。它们用于追溯决策和历史证据,不再定义当前产品拓扑、阶段状态或最新测试数字。 + +当前入口: + +- UI 产品拓扑:[`../../next-ui-current-state.md`](../../next-ui-current-state.md) +- UI 后续施工:[`../../next-ui-development-roadmap.md`](../../next-ui-development-roadmap.md) +- Chat Kernel 当前执行合同:[`../../chat-kernel-deep-decoupling-roadmap.md`](../../chat-kernel-deep-decoupling-roadmap.md) +- Chat Kernel 当前阶段审计:[`../../chat-kernel-vd7-final-audit.md`](../../chat-kernel-vd7-final-audit.md) + +归档内容: + +- `chat-kernel-rendering-roadmap.md`:已完成的 C0-C7 路线。 +- `chat-kernel-deep-decoupling-roadmap-history.md`:D0-D7 施工历史和中间状态。 +- `chat-kernel-vd-roadmap-review.md`:按时间追加的对抗式审查日志。 +- `next-ui-webawesome-roadmap.md`:已结束的并行 presentation 建设路线。 +- `ui-applications-webawesome-migration-plan.md`:已停止的整页迁移计划。 diff --git a/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-deep-decoupling-roadmap-history.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-deep-decoupling-roadmap-history.md new file mode 100644 index 000000000..1729f4c41 --- /dev/null +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-deep-decoupling-roadmap-history.md @@ -0,0 +1,214 @@ +# VCPChat Chat Kernel 深度解耦路线 + +> 归档说明(2026-08-21):本文保存 D0-D7 的施工历史和阶段增量,包含已经过期的中间测试数字,不再作为当前执行状态权威。当前合同见 `../../chat-kernel-deep-decoupling-roadmap.md`,当前证据见 `../../chat-kernel-vd7-final-audit.md`。 + +> vD5/vD6 最终收口(2026-08-21):vD5、vD6 已达到范围内退出条件,vD7 仍为 BLOCKED。`renderer.js` 现只承担主聊天 composition/lifecycle orchestration;设置/presentation、主题、TTS、Flowlock、辅助 preload 事件、forward、DOM listener、附件瞬态状态和发送/中止策略均由 named owner 持有,所有 owner 按注册逆序等待 dispose。仓内已停止发布和读取 `window.globalSettings`、presentation helpers、`window.checkMessageFilter` 与 bubble-layout helper;保留冻结且不可替换的 `window.VCPMainChatState`、`window.MainChatCommands`、`window.VCPAppearanceStudio` 公共 facade,并由 consumer ledger 记录 owner、生产消费者、动态 smoke 和退役条件。consumer report 另清点 164 个 Classic/feature-local ambient facade 名称作为兼容债务,故 vD6 不表示全仓 `window.*` 清零。最终当前主机矩阵为 `artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json`,六行通过;本轮最新自动化为 Chat Kernel 140/140、UI System 88/88。缺 30-60 分钟人工 soak 和跨 Windows/打包/GPU-DPI 证据,不能宣称 D0-D7 最终完成。 + +> 行为等价基线(2026-08-21):未登记差异默认视为回归。当前自动化覆盖主聊天发送/流式/取消/重试、历史与 topic restore、附件、设置/主题/presentation、通知、desktop push、Voice/Rust、Next/Classic 与插件命令入口,并比较 terminal/persistence、DOM、焦点/ARIA、listener/resource 和 crash/reload 终态。本轮真实 Electron 发现 startup restore 在 topic DOM consumer 完成前返回;已让 `ChatManager.selectItem()` 等待 topic projection 并在等待后重检 selection generation,focused regression 与真实序列通过。首轮最终矩阵另记录一次 Puppeteer protocol timeout;随后完整串行矩阵通过,该超时只作为测试基础设施波动证据保留。 + +> 当前证据增量(2026-08-20):Chat Kernel 129/129,UI System 86/86;lifecycle stress 3 warmup + 60 measured 稳定;辅助窗口 reload 与三轮 crash recovery 真实序列通过(每轮 opt-in,合计 72 actions / 42 requests)。以下较早数字均为历史追溯记录,不能覆盖当前状态;D7 仍未完成。 + +> Windows 矩阵增量(2026-08-20):新增 `npm run test:windows-matrix`,当前 Windows 主机六项串行入口全部通过;证据写入 `artifacts/windows-matrix/`。这只是当前主机的可重复矩阵,不是多版本 Windows/安装包矩阵,也不替代 30–60 分钟人工 soak。 + +> vD5 增量(2026-08-20):`MainChatSettingsOwner` 已接管主窗口 settings 对象的 replace/update/ref/snapshot/dispose;`MainChatThemeOwner` 已接管初始主题与 presentation transition/persistence/rollback;`DomListenerOwner` 已收口 presentation quick switcher、字体/布局/用户气泡设置控件、emoticon trigger、quick-new-topic bridge 以及 `setupEventListeners` 的主聊天消息区/发送/输入/附件 listener,并纳入 renderer teardown。global-settings UI 语义保持不变,Electron UI Apps 24/24 复跑通过。Flowlock 与 forward 迁移后的 renderer 内联实现已物理删除,consumer guard 与主聊天 24-action 复跑通过。辅助窗口 held-stream reload 与 opt-in crash-recovery 场景已加入主聊天序列并通过(crash 矩阵三轮合计 72 actions / 42 VCP requests);lifecycle stress 当前通过 3 warmup + 60 measured,资源指标稳定。当前 Chat Kernel 为 129/129,D7 仍未完成。 + +> 状态(2026-08-20,复核):D0、D1 已完成;D2、D3 已具备真实协调器、operation identity、主聊天/独立 Surface/辅助窗口消费者和 Electron 终态证据。StreamProjection 已按 Surface 工厂化,每个 Surface 拥有独立 DOM projection、固定 conversation identity、terminal route 和 quiescent dispose;独立聊天不再读取主窗口 selection/history。D4 已建立 RenderDependencies、SurfaceConversation 和显式 DOM realm/service capability closure。近期增量已加入 operation-scoped projection runtime、owned realm scheduling、Loom/VCP log/status、agent-settings reload、history watcher、group-topic 和 Flowlock 的 owned subscription teardown、嵌入应用 stale session 对账,并收紧 MessageRenderer 的 root/style owner;StreamProjection 现在要求 owning root、显式 view authority 和 transient history provider。D5 正在进行:MainChatSurfaceAdapter 已组合主聊天 stream/operation 生命周期,主窗口发送状态由真实 stream consumer 的 terminal settlement 更新,不再由 DOM projection 反向控制;TtsSurfaceOwner 已接管生产 TTS 状态和订阅,MainChatAuxiliaryEventOwner 已接管 Loom/VCP log/status/group-topic,MainChatFlowlockOwner 已接管 Flowlock command/request 和 RPC 响应,ForwardMessageOwner 已接管 forward modal、目标选择、原始消息读取和附件转发,renderer.js 仍保留主题、设置、Electron 和部分事件 wiring。D6 已删除三个旧全局 facade、全部 legacy history IPC fallback、`initStreamManager` 命名的伪单例入口、生产 `window.ensureAudioContext`、无消费者的 `window.showForwardModal` 与 `window.setChatPresentationMode`;剩余 renderer legacy methods、完整 operation identity 下沉和 ambient DOM 路径仍待收口。D7 未完成。最新本地复核:Chat Kernel 129/129、UI System 85/85、`guard:next-delta` 和 `guard:chat-kernel-consumers` 通过;`npm run test:electron-ui-apps` 24/24 通过,`npm run test:electron-lifecycle-stress` 3 warmup + 60 measured 通过,辅助窗口 crash/reload opt-in 三轮合计 72 actions / 42 requests,主聊天 24-action 通过。辅助窗口更长矩阵、完整 Windows 矩阵、30–60 分钟人工 soak 和 D7 逐项审计仍未完成,不能宣称 D7 或最终解耦完成。 +> 规范依据:`C:\VCP\vchat-develop\deepseek-harness\AGENTS.md`、`docs/event-producer-consumer.zh.md`、`docs/defensive-patterns.zh.md` 和 `.agents/skills/dsh-code-review/SKILL.md`。 + +> vD0–vD7 对抗式复核已记录在 [`chat-kernel-vd-roadmap-review.md`](./chat-kernel-vd-roadmap-review.md)。该复核将 D5 的退出对象收紧为 composition-root responsibility,而不是文件大小;静态 consumer report、单元测试和历史 Electron 记录不能单独证明真实消费者或最终边界。2026-08-20 已修复 `renderer.js:558` 的 TTS capability 作用域错误并通过 Electron UI Apps 24/24、lifecycle stress 3 warmup + 60 measured;辅助窗口 crash/reload 多轮证据、renderer owner 迁移和人工 soak 仍未闭合。 + +## 目标 + +把当前仍依赖 `window.*`、固定 DOM、Electron API 和主窗口单例的聊天实现,逐步收敛为三个可验证层: + +```text +Chat Domain +├── ChatContext / repository +├── StreamSession +└── durable history and domain events + +Content Runtime +├── message normalization +├── protocol transforms +└── immutable render model + +Surface Adapter +├── DOM projection +├── input/focus/ARIA +├── theme and slots +└── owner-scoped teardown +``` + +目标不是删除主窗口或重写消息协议,而是让同一份领域结果可以被主聊天、只读聊天和交互聊天分别消费。`renderer.js` 最终只应负责组装主窗口依赖和 Surface,不再拥有流式业务状态;`messageRenderer.js` 最终只应负责 DOM 投影,不再读写主窗口全局状态。 + +## 当前耦合审计 + +D0 的可重复基线由 `npm run guard:chat-kernel-consumers` 生成到 `docs/chat-kernel-consumer-report.json`。该报告把生产入口和测试入口分开列出;报告中的生产引用不是删除清单,而是后续迁移的消费者责任表。 + +### renderer.js / messageRenderer.js + +- `renderer.js` 仍然是主窗口 composition root,同时把固定 DOM、`window.*` facade、Electron API、主题、设置、聊天管理和事件监听连接在一起。 +- `messageRenderer.js` 仍通过 `mainRendererReferences` 读取 history、selected item、settings、Electron API 和主窗口 helper;它虽支持显式 root,但不是纯 renderer adapter。 +- `chatManager.js` 仍同时负责选择、历史、文件、VCP 请求、业务副作用、DOM 查询和全局按钮状态。 +- `window.chatManager`、`window.messageRenderer`、`window.streamManager` 已完成生产迁移并删除;consumer gate 对全生产源码做负向扫描并对 compatibility-global 计数强制为零。其他 renderer legacy methods 仍需逐项审计,不能把三个 facade 的退役误认为 renderer 已纯化。 + +### StreamManager + +`modules/renderer/streamManager.js` 当前拥有每个 StreamProjection 的短生命周期 DOM 投影状态;实时 transport、durable commit 和 Desktop push 已由其他 owner 负责。仍需继续收口的风险是: + +- MessageRenderer 的流式 DOM 更新、chunk 队列和 Surface-local active state; +- legacy render helper 的少量主窗口引用; +- Surface-owned scroll/animation and realm access. + +这不是单纯的“文件太长”问题,而是多个 owner 共用一组可变状态。后续拆分必须先建立事件和 owner 语义,再移动代码。 + +## Harness 对应原则 + +1. **持久事实与实时协调分离**:可回放的消息/流终态进入 history repository;发送按钮、取消、重试和当前运行状态属于实时 Surface 协调,不混入持久 history。 +2. **Definition / Provider / Consumer 同时存在**:每个新 session、event 或 service 都必须列出生产方、真实生产消费者和测试入口;只有测试调用的 facade 不得进入公共面。 +3. **事件不是万能状态**:事件传递事实变化,不替代当前 snapshot;每个事件必须有明确顺序、终态和丢弃规则。 +4. **dispose 达到 quiescence**:取消只表示请求停止;Surface dispose 必须等待 reader、render queue、history save 和 desktop push 的 owner work 停稳,迟到事件保持静默。 +5. **跨边界显式注入**:Domain 不读取 `document`、`window` 或 Electron;DOM、通知、焦点和桌面能力由 Surface capability closure 注入。 +6. **先迁移消费者再删除 facade**:旧 facade 只有在生产搜索、动态入口和 Electron smoke 都证明没有消费者后才能删除。 + +## 分阶段计划 + +| 阶段 | 施工内容 | 必须保留的行为 | 退出证据 | +| --- | --- | --- | --- | +| D0 基线冻结 | 记录 renderer/stream 的调用图、状态变量、事件、定时器、Electron API 和 owner;为每个出口标记 production/test/legacy | 主聊天现有发送、流式、切换、历史、附件行为 | 静态 consumer report、主聊天 24-step、生命周期 baseline | +| D1 Stream protocol | 新建纯函数 `StreamSession`/`StreamState`:`start → chunk → terminal/error/cancel → persist`;只处理 request identity、generation、terminal arbitration 和 immutable events | SSE 解析、chunk 顺序、完成/失败/取消语义不变 | unit 覆盖逆序、重复 terminal、断线、取消、迟到 chunk;无 DOM/Electron import | +| D2 Stream coordinator | 将 reader、AbortController、session registry、per-topic save queue 移入 coordinator;按 session owner 管理 desktop push 和 history persistence | 当前 topic 切换、后台流、并发逆序保存和 retry | 真实 VCP Electron fixture;每个终态等待真实 Promise;dispose 后 active sessions/readers/queues 为 0 | +| D3 Stream consumers | 主聊天和独立 Surface 分别订阅 stream events,转换为各自 render command;发送按钮、通知、桌面 push 由明确 consumer 消费 | 主窗口按钮、通知、桌面 push、流式 DOM 与现状一致 | producer/consumer matrix;缺 consumer 的 event gate 失败;主聊天与独立 Surface 双入口通过 | +| D4 Renderer capability closure | 将 `messageRenderer` 的 marked/settings/helper/electron 依赖收窄为显式 `RenderDependencies`;root、history snapshot、message model 作为参数 | Markdown、工具结果、附件、Mermaid、媒体、编辑和清理行为不变 | Content/DOM 分层测试;renderer adapter 不访问全局 selected/history;静态 gate 禁止新增 `window.*` | +| D5 Main Chat Adapter | 将 `renderer.js` 的 composition、固定 DOM 查询、事件绑定和主窗口 helper 组装到 `MainChatSurfaceAdapter`;ChatManager 只保留 domain commands | 主窗口布局、键盘、主题、设置、通知、插件 Loader 行为不变 | Electron 主聊天完整序列、真实键盘/ARIA 终态、reload/crash recovery、owner diagnostics 稳定 | +| D6 Facade retirement | 迁移 `window.streamManager`、`window.chatManager`、renderer legacy methods 的生产调用者;为保留的兼容调用增加明确 owner/期限 | 仅保留确有生产消费者的兼容入口;测试夹具显式 opt-in | consumer search + dynamic smoke;无生产引用的 facade、字段和 preload API 删除 | +| D7 Final boundary | 删除重复 state/cache、合并唯一 terminal/persistence authority、更新架构和门禁 | 所有 D0 行为矩阵保持;不迁移业务子页面、不改变插件协议 | 全部 Windows 矩阵、30–60 分钟人工 soak、P5 稳定周期入口;文档与 gate 同步 | + +## D0–D2 当前施工(部分完成,尚未 exit-ready) + +`modules/chat/streamSession.js` 是纯协议层,只负责 session identity、chunk 顺序、四种终态归一化、单终态仲裁和 subscriber 隔离;不读取 DOM、Electron、history 或 desktop push。`modules/chat/streamCoordinator.js` 负责 owner-scoped 实时协调:私有 lease 决定提交权,同一 topic 的不同 message 可并发,直接 Coordinator consumer 可用 replacement identity 撤销旧 attempt;VCP Bridge 将生产 message id 视为单次身份,不提供同 id retry。per-conversation persistence queue、AbortController、owned cleanup 与 `done`/`dispose()` 均等待真实 Promise,不提供全局 registry、Store 或 `whenIdle()`。 + +D2 的公共 terminal 只在 transport 停稳和 persistence settle 后发布一次。持久化失败形成唯一 `failed` outcome,并保留原 transport outcome 作为只读证据。`ChatHistoryPersistence` 负责 repository read、按 message identity 合并、过滤 renderer-owned pending entry 和 durable write;Coordinator 负责 per-conversation serialization。StreamManager 不再暴露 durable commit 或 legacy finalize facade。投影失败不会以 completed no-op 结束,缺失 initialization 会释放活动消息并进入 persistence failure。StreamManager 仍拥有 `pending/ready/finalized` DOM/history 投影状态,因此全仓唯一 terminal authority 尚未达到退出条件。 + +## D3-D6 当前施工(vD5/vD6 exit-ready,vD7 未完成) + +- 主窗口 preload 事件进入 `VcpStreamBridge → StreamCoordinator → MainChatStreamConsumer`,旧 `renderer.js` 的 start/data/end/error switch 已删除;VoiceChat 和 Rust Assistant 已接入辅助窗口 runtime,Flowlock streaming 由主窗口 bridge 消费。重新生成和本地中止兜底也进入同一 Bridge operation,不再直接启动或 finalize StreamManager。独立交互 Surface 继续共用主窗口 transport,但拥有独立 root、operation-specific `done/cancel` 和可撤销 route lease;主窗口切换话题不会撤销独立 root 的投影权,独立取消也不会中止并发的主窗口请求。它尚未拥有独立 transport coordinator。 +- `RenderDependencies` 取代带静默默认值的可变引用袋;root、state refs、repository、transport、Markdown、feedback、DOM realm、Pretext、Flowlock 和 commands 在 mount 时显式提供并冻结。`messageRenderer` 的主渲染路径已消费这些 capability,但仍有少量历史 DOM/兼容调用待 D5/D6 审计。辅助窗口现在显式提供短会话 `MemoryChatRepository`,不再依赖缺失 repository 的静默 fallback。 +- `MainChatSurfaceAdapter` 已组合主聊天 Surface、stream route、bridge、capability provider 与 teardown;终态副作用和错误 DOM 已从 `renderer.js` closure 移入 adapter。群聊已持久化的 `full_response`/`remove_message` 事件由显式 `NonStreamingEventConsumer` 消费。`renderer.js` 只保留 DOM 查询、capability/provider 构造、manager/owner/adapter 装配、mount 与逆序 dispose;主题、设置/presentation、TTS、Flowlock、辅助事件、forward 和 DOM listener 均有 named owner。D5 已通过责任门禁与真实 Electron 验收。 +- pending assistant、后台历史读取、reply ordering 和终态 message model 已移入纯 `StreamTransientHistory` provider;StreamProjection 只调用其 `prepare/finalize/discard` 并投影返回的只读 message snapshot,不再直接读取 repository 或改写 history 数组。`ChatHistoryPersistence` 继续作为 Coordinator 之后的唯一 durable commit capability;缺少 root、view authority 或 transient provider 时会 fail-fast。 +- 生产 `streamOperationId` 现在从 preload 事件桥传入主 Surface consumer、流投影初始化、chunk 和 terminal 校验;`messageId` 仍是 DOM 显示身份,但不再是唯一的生产操作证据。相同 message 的重试由 Bridge 的 operation identity 隔离,Projection 会拒绝不匹配的迟到 chunk/terminal。 +- StreamProjection 的 active、累积文本、chunk queue、初始化、上下文、DOM cache、scroll timer 和 segment state 已通过 operation-scoped runtime key(`streamOperationId::messageId`)隔离;重试先撤销旧 operation,再建立新 owner。主聊天完整 20-run/489-action 矩阵已通过,覆盖逆序并发、reload/crash 和终态发送状态。 + +此前验证记录(历史):`test:chat-kernel` 107/107、`test:ui-system` 85/85、Electron UI apps 24/24、主聊天完整 20-run/489-action、生命周期压力 3 次预热 + 20 次循环及 connected-DOM 聚焦回归、`guard:next-delta`、`guard:design-subtraction`、`guard:classic-retirement`、`guard:chat-kernel-consumers` 均通过。主聊天真实序列另覆盖独立 Surface 在主话题切换后的持续投影、主/独立并发隔离取消、Flowlock 非流式唯一 durable terminal、VoiceChat/Rust Assistant 成功终态、pending close cancellation、无 thinking 残留和最终新话题持久化。真实 Electron smoke 曾捕获 composition root 未把 transient provider 传入 RenderDependencies 的启动失败,已将该 capability 纳入 fail-fast closure 并复跑通过。D7 仍需辅助窗口并发/reload/crash recovery 的独立长矩阵与 30–60 分钟人工 soak 证据;当前复核结果以上方状态段为准。 +- VoiceChat 与 Rust Assistant 的流终态先提交到显式的 transient session repository;窗口关闭会取消并等待真实 stream operation Promise,再由同一窗口 owner 持有的 `ChatHistoryMutationAuthority` 把完整会话写入新建的 durable topic。真实 Electron 测试曾发现局部作用域 authority 导致“创建空话题后静默丢历史”,现已修复并对成功内容、thinking 清理和 durable 文件做断言。窗口在最终 topic 创建前进程异常退出仍可能丢失短会话,这是 D7 soak 与产品恢复策略需要继续评估的已知限制。 +- `window.chatManager`、`window.messageRenderer`、`window.streamManager` 已无生产引用并完成删除。consumer gate 自动扫描全部生产 `.js/.mjs/.html`,新增文件若重新读取这些全局会确定性失败;仍保留的 renderer legacy methods 必须逐项提供生产消费者证据。 +- ChatManager、TopicListManager、SearchManager 与 StreamManager 已删除 `allowLegacyHistoryFallback`;生产和测试都必须显式注入 `ChatRepository`,不存在绕回 preload `save/getChatHistory` 的隐藏后门。Flowlock 非流式保存同样通过共享 mutation authority,门禁禁止重新引入直接 history IPC。 + +## Stream Session 目标接口 + +D6 最新增量已移除主聊天 selection 与 topic-readiness 的无界全局 facade:`VCPMainChatState` 是 composition root 创建的只读 consumer,`TopicSelectionReadiness` 由 TopicListManager 显式注入;辅助 Voice/Rust 并发场景已覆盖 context binding、独立 terminal 和分别关闭。D5/D7 仍以完整 renderer composition 收口和人工 soak 为退出条件。 + +后续 legacy 审计已完成真实消费者迁移:Middle-click 与 Context-menu 的转发/音频入口通过 Surface capability 注入;settings/filter/presentation 内部消费者也已改用显式 capability。保留 `VCPMainChatState`、`MainChatCommands` 与 `VCPAppearanceStudio` 公共 facade,并在 consumer report 中登记。历史 lifecycle stress 已通过 3 warmup + 60 measured cycles;本次对抗式修复另复跑 3 warmup + 20 measured,不能把历史 60-cycle 误记为当前补丁重跑。CDP `Runtime.callFunctionOn` 曾出现一次协议超时,完整串行矩阵随后通过,但这仍不能替代人工 soak。 + +第一版只保留真实消费者需要的最小接口,不做通用 Store: + +```js +const session = streamCoordinator.start(request, { + onEvent: event => surface.consume(event), + signal, +}); + +await session.done; // terminal outcome, not merely reader closed +await session.dispose(); // abort, drain, detach, and persist owner work +``` + +事件采用封闭 discriminant,至少包括: + +- `started`:带 session id、conversation key 和 generation; +- `chunk`:只带规范化文本/协议块,不带 DOM; +- `completed`:带最终 message model 和 persistence result; +- `failed`:带可重试错误和是否已持久化; +- `cancelled`:带取消来源; +- `discarded`:Surface 或 generation 已失去提交权。 + +`reader.close`、AbortSignal、网络断开和 renderer dispose 都必须归一到上述终态之一;同一 session 只允许一个 terminal outcome。 + +## 关键禁止事项 + +- 不把 `StreamSession` 做成第二个 `window.streamManager`。 +- 不新增全局 `whenIdle()`、万能 Store 或只读 diagnostics facade。 +- 不让 stream coordinator 直接操作 DOM、焦点、按钮、Toast 或 CSS。 +- 不让 renderer adapter 直接读取 `currentSelectedItemRef`、`currentChatHistoryRef` 或 `window.*`。 +- 不在没有真实 Surface consumer 的情况下提前删除兼容 facade。 +- 不把持久 history 当作流式 UI 的逐 chunk 状态源;终态保存必须有明确 commit point。 + +## 测试矩阵 + +### 纯函数/单元 + +- chunk 顺序、重复 terminal、空内容 terminal、工具/思维链/代码块协议隔离; +- cancel、disconnect、reader error、dispose during await; +- 两个 topic 逆序完成,只有每个 topic 的最新 durable history 被保留; +- event subscriber 异常隔离,其他 consumer 仍收到终态; +- generation 替换后迟到 chunk 不再有提交权。 + +### 真实 Electron + +- 主聊天成功/失败/取消/断线/重试; +- 独立只读和交互 Surface 同时运行; +- close while pending、reload/crash while pending、重复 dispose; +- 主题、焦点、ARIA、通知、桌面 push 与 stream terminal 对齐; +- 12 个 Classic 子页面保持无新 runtime/consumer。 + +### 门禁 + +- `check:next-delta-contract`:Domain/Stream 不得依赖 DOM/Electron;每个 event 有 producer/consumer;禁止新增全局 stream facade; +- `check:design-system-boundary`:所有共享文件有审查理由; +- consumer report:旧 facade 的生产引用为零后才允许删除; +- lifecycle inspector:session、reader、timer、save queue、listener、Surface owner 在 dispose 后归零。 + +## 完成定义 + +只有同时满足以下条件,才能把 renderer 深度解耦标记完成: + +1. Stream Session 和 coordinator 不导入 DOM、Electron 或主窗口 helper; +2. MessageRenderer 只接收显式 root、render model 和 capability closure; +3. MainChatSurface 是生产组装者,不再让 `renderer.js` 充当业务状态中心; +4. 每个保留 facade 都有至少一个可定位的生产消费者,否则删除; +5. 真实 Electron 证明所有 terminal、persist、cancel、dispose 和 late-result 场景; +6. 完整 Windows 矩阵和人工 soak 通过; +7. 文档、静态 gate、consumer report 和代码在同一提交组中同步。 + +## 2026-08-20 收口复核与人工 soak 入口 + +当前 Chat Kernel 为 129/129,UI System 为 85/85;当前 Windows 主机矩阵六项串行通过,生命周期压力为 3 次预热 + 60 次测量,辅助窗口 reload/crash 入口已覆盖。上述结果仍不满足第 6 项:矩阵仅覆盖一台 Windows 主机,未覆盖多版本、打包安装、GPU/DPI 组合,且尚无人工作业记录。 + +新增 `npm run test:manual-soak`(可用 `VCPCHAT_MANUAL_SOAK_MINUTES=30` 和 `VCPCHAT_MANUAL_SOAK_INTERVAL_SECONDS=60` 调整)。该入口启动真实 Electron、周期采集 renderer lifecycle/heap/process/error 快照,接受 stdin 的 `finish`、`fail`、`abort`,并写入 `artifacts/manual-soak/.json`。产物固定为 `manual_observation_required`,不会把“启动并退出”或自动计时误记为通过;操作员必须逐项记录主聊天流式、历史切换、附件、主题、通知/桌面 push、VoiceChat、Rust Assistant、reload/crash、Classic 子页面和插件协议结果。 + +本轮决策:人工 soak 采用观察日志与人工检查清单分离。自动脚本只能证明入口、采样和清理可运行,不能替代人的交互覆盖或把短时自动压力测试升级为 D7 证据。`window.globalSettings` 与 presentation/filter helpers 已迁移删除;TopicListManager 的其它 legacy service 读取不属于本次已退休 facade 集合,继续由后续边界审计跟踪。 + +本轮 D5 增量还将 TopicListManager 的 `uiManager` 与 `itemListManager` 作为显式 capability 注入,移除该模块对这两个 window service 的生产读取;Next delta baseline 已同步更新并通过门禁。剩余 UI service ambient paths(如 main-chat commands、notification renderer 和 appearance studio)仍需按真实消费者逐项迁移,不能以本次 TopicListManager 收口外推为全部完成。 + +数字更正(2026-08-20):新增 UI manager lifecycle focused test 后,`test:ui-system` 当前为 86/86;文档中早先的 85/85 仅表示该测试加入前的历史运行。 + +`MainChatCommands` 现由 composition root 注入 UI、过滤、通知、appearance 和 tab capabilities;主聊天真实序列 `24 actions / 25 requests` 复跑通过。该改动只收口命令内部的 service 获取,不删除 `window.MainChatCommands` facade,因为 Next shell、Classic 菜单和插件命令仍有真实生产消费者。 + +`notificationRenderer` 现接受 filter capability 与 `DomListenerOwner`;焦点清理和周期过期清理在主 renderer 中由 owner 持有,降低 process-lifetime timer 风险。兼容页面未提供 owner 时仍有 fallback,因此通知模块和 appearance studio 的全部 ambient 读取仍未达到 D5 完成定义。 + +`uiManager.init()` 现接受主窗口 `DomListenerOwner`,通过 capture 收口 sidebar/navigation listener,数字时钟使用 owner-scoped recursive timeout。该变更证明 UI manager 的 teardown 资源可被主 Surface 回收,但 settings/item-list service 的真实兼容读取仍保留,不能宣称 ambient 依赖已全部消除。 + +UI manager 的 settings/item-list 读取现由 renderer composition root 显式注入,旧 window fallback 仅用于兼容页面;主聊天 24-action 序列与两个静态 gate 通过。该 capability 收口仍不代表 appearance studio 或所有 UI service 已完成迁移。 + +Appearance Studio 现由 composition root 注入 settings、appearance engine、UI manager 与 presentation capability,预览/回滚/保存路径优先走显式对象;`test:appearance-studio` 和主聊天序列通过。公共 `window.VCPAppearanceStudio` 仍被 Next shell 与设置 UI 消费,保留 facade 不代表 renderer 业务状态仍由它拥有。 + +Appearance Studio 的 revision、settings、toast/modal 和 presentation 读取进一步统一为 capability helper;旧 window 访问只作为 fallback。该增量通过 appearance 测试与 Next delta gate,但不改变公共 facade 的保留决策。 + +最新 `npm run check:ui-system` 全链路通过:静态边界、Classic parity/retirement、Next delta、consumer gate、UI applications、stylelint、UI System 87/87、appearance engine/studio、Next tab lifecycle、WebAwesome adapter 和 lifecycle/topic tests 均通过。该证据仍不覆盖人工 soak、多版本 Windows 或打包安装矩阵。 + +UI manager 的设置标签中键 modal 操作也已改为由 composition root 注入 `uiHelper`,旧 window helper 只作 fallback;主聊天序列和静态门禁通过。 + +UI manager 现有幂等 `dispose()`,由主 renderer 的 `ownedRendererSubscriptions` 调用,清除 capability 与 DOM 引用并拒绝 dispose 后重新初始化;这补强了主 Surface 的 quiescent teardown 证据。 + +人工 soak harness 的 Windows 清理现已复用精确 PID 进程树终止策略,避免 Electron renderer/GPU 子进程残留污染后续矩阵;短时 smoke 仅证明入口和清理可运行,产物仍要求人工逐项审核。 + +最新 `npm run test:windows-matrix`(2026-08-20 20:49Z)在当前 FlowX13 Windows 主机六项全部通过,artifact 为 `artifacts/windows-matrix/2026-08-20T20-49-30-813Z.json`;resize CDP 仍显式 skipped,矩阵范围仍限单机。 diff --git a/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-rendering-roadmap.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-rendering-roadmap.md new file mode 100644 index 000000000..a1f9fe928 --- /dev/null +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-rendering-roadmap.md @@ -0,0 +1,86 @@ +# VCPChat Chat Kernel、Renderer 与受控 UI 插件化路线 + +> 归档说明(2026-08-21):本文是已完成的 C0-C7 历史路线,不再维护当前阶段状态。当前 D0-D7 状态以 `../../chat-kernel-vd7-final-audit.md` 为准。 + +> 状态:C0–C7 已完成并通过 Windows 最终矩阵;后续扩展继续受本路线合同约束。 +> 规范依据:`C:\\VCP\\vchat-develop\\deepseek-harness\\AGENTS.md`、`docs/defensive-patterns.md`、`docs/testing.md` 及相关 skill。 + +## 目标 + +让主聊天能够在保持现有行为的前提下,逐步复用于只读独立聊天页、嵌入式聊天应用和展示型 presentation skin。目标不是复制一套业务实现,也不是把任意 DOM/CSS/IPC 暴露给插件,而是建立一个有真实消费者、最小权限和可证明销毁的边界。 + +## 当前审计结论 + +`renderer.js`、`modules/messageRenderer.js`、`modules/chatManager.js` 和 `modules/event-listeners.js` 在文件层面分开,但运行时仍共享全局可变状态、固定 DOM ID、`window.*` 单例和 Electron API。现阶段不能安全地把 `renderer.js` 直接作为第二个页面的渲染器复用。已有 `contentPipeline` 等内容模块是可利用的低风险边界,但流式、历史、附件和 DOM 生命周期仍与主窗口耦合。 + +## 目标拓扑 + +```text +Chat Domain Kernel +├── ChatContext / Message model +├── History repository +├── Stream session +└── read-only state events +Content Runtime +├── markdown/tool/thought parsing +├── attachment representation +└── stream incremental assembler +Chat Renderer Adapter +├── mount/update/remove/renderBatch +└── destroy → quiescence +Chat Surface +├── MainChatSurface +├── StandaloneReadOnlyChatSurface +├── EmbeddedChatSurface +└── Presentation/Galgame skin +``` + +Domain 不依赖 `document`、固定选择器或 Electron;内容运行时优先纯函数;Renderer Adapter 接收显式 `root` 与最小依赖;Surface 负责布局、输入、主题、焦点和 owner 生命周期。 + +## 分阶段施工 + +| 阶段 | 内容 | 真实消费者与退出条件 | +|---|---|---| +| C0 基线 | 记录调用图、主聊天序列、流式/历史/附件行为 | 现有 Electron 与 UI 测试通过;无行为改动 | +| C1 ChatContext | 将 selected item、topic、history 组合为显式上下文,保留兼容读写边界 | MainChatSurface 是唯一消费者;旧全局引用数量下降并有回归证据 | +| C2 Repository | 完成(生产路径):ChatManager、TopicList、Search、Stream、Message Edit 均使用同一 `ChatRepository`;缺失时 fail-fast。旧 Electron history API 仅可由测试夹具显式开启 | 真实保存/加载/错误测试;所有生产历史路径使用同一 repository;Domain 不出现 Electron | +| C3 Content Runtime | 已完成:`normalizeMessage`、DOM-free `createRenderModel`、Mermaid/entity transforms、chunk 提取、stream assembler、附件归一化及工具结果/思维链/工具请求/代码块顺序协议均有纯函数 seam;MessageRenderer 仍保留最终 HTML/DOM 投影 | 30 项 Chat Kernel 纯函数/适配测试;主聊天与独立 Surface Electron 序列通过 | +| C4 ChatDomRenderer | 已完成:显式 root、pending quiescence、第二 root、listener disposer、stream transient cleanup、内容延迟工作句柄、Three.js observer/renderer、Mermaid、媒体和附件交互 listener 均有 per-root cleanup | 主窗口与独立 Surface 接入;生命周期压力中 listener/timer/DOM/pending work 稳定、detached roots 为 0 | +| C5 只读 Surface | 已完成:`standalone-chat-history` 已注册为普通 Launchpad 内部应用,Electron smoke 证明独立 root、只读、焦点和关闭 teardown | Electron 真入口、主题/焦点/关闭和 teardown 完整 | +| C6 交互 Surface | 已完成:`standalone-chat-compose` 通过真实 VCP fixture 覆盖成功、失败、取消、断线、重试、pending close、重复关闭和迟到终态隔离 | 使用真实请求/Promise/ARIA 终态;关闭后 owner 与 DOM 不再恢复权限 | +| C7 受控插件化 | 已完成:manifest/loader 已接入独立聊天生产消费者,命名 slot、只读 state、token theme、skin mount、状态更新、provider 失败回滚、重复/迟到注册和统一 teardown 均有测试 | 插件只能读正式状态、无任意 DOM/IPC;register/use/dispose 与卸载后静默均可证明 | + +## Harness 决策门槛 + +- 每个 capability 必须同时有 Definition、Provider、Consumer;没有生产消费者的 facade 删除。 +- registration 是 effect,必须返回 disposer;dispose 需达到 quiescence,迟到 Promise 不得重新取得提交权。 +- 测试等待真实 operation Promise、结果事件或 DOM/ARIA 终态,禁止全局 `whenIdle()` 和固定延时猜测。 +- 插件优先使用命名 slot 和 token,不使用任意 selector;presentation skin 不得伪造业务状态、接管焦点或 Escape。 +- 每一阶段独立可回滚;先由主窗口成为第一个消费者,再新增独立 Surface,避免双轨实现。 + +## 暂不做 + +不迁移业务子页面、不重写消息协议、不替换 `ChatManager`、不开放任意 IPC/CSS 注入、不引入万能 Store;这些只有在真实消费者和测试同时出现时才进入后续 PR。 + +## 当前验证证据(2026-08-19) + +- `npm run test:chat-kernel`:30/30;包含冻结 render model、DOM-free Mermaid/entity transform、附件协议归一化、工具结果/思维链/工具请求/代码块顺序隔离、slot/provider rollback、卸载后迟到状态静默和交互取消幂等回归。 +- Next delta gate 还会检查每个独立 root 的 animation/visibility teardown,以及 Three.js renderer disposal 证据。 +- `npm run guard:next-delta`:通过;共享边界 hash 已按审查后的 ChatManager、MessageRenderer、StreamManager 变更更新。 +- Next delta gate 现在额外验证 TopicListManager、SearchManager、MessageRenderer/StreamManager 在 renderer 生产入口均收到同一 `ChatRepository`。 +- Mermaid placeholder 和 HTML entity 解码已移到 `modules/chat/contentTransforms.js`,不再依赖 `document.createElement`;MessageRenderer 通过依赖注入消费它。 +- `npm run test:electron-ui-apps`:24/24;包含只读/交互独立聊天 root、插件展示和 teardown。 +- `VCPCHAT_SEQUENCE_RUNS=3 npm run test:electron-main-chat-sequences`:72 actions、28 个 VCP 请求;真实 fixture 覆盖独立聊天成功、失败、取消、断线、pending close/迟到终态,以及主聊天流式、切换、恢复和逆序并发持久化。 +- Stream terminal now explicitly flushes and awaits the per-topic history save; the independent-surface fixture waits for the composer Promise terminal (`aria-busy=false`) before starting the next operation. +- `npm run test:electron-lifecycle-stress`:3 次预热 + 20 次循环;nodes/listeners/processes/scopes/resources 保持稳定,detached roots 为 0。 +- `tests/stream-manager-terminal-cleanup.test.js`:4/4;流式终态失败和 discard 清理通过。 +- `npm run check:ui-applications`:通过;81 个业务文件、12 个上游 Classic 子页面边界保持不变。 +- `npm run pack:check`:通过;Web Awesome 离线闭包可复现。 +- 延迟高亮与 Pretext idle 回调现在以消息 root 持有句柄,`cleanupMessageDomResources` 在 Surface 销毁时取消它们;独立聊天展示层不再写入 inline style。 +- Three.js WebGL renderer 的 MutationObserver 现在与 renderer 同属一个 owner,dispose 时断开;presentation skin 可通过受控 update 接收状态变化,loader 负责订阅与卸载。 +- C4/C7 增量审查补充了 Mermaid viewer、附件图片/文件/删除按钮 listener 的显式 disposer;插件 provider 中途失败会回滚已注册贡献,卸载后重复 disposer 与迟到 state update 不再重新取得权限。 +- `npm run test:electron-ui-apps`:24/24;一次并行启动中的抽屉首帧断言曾出现偶发失败,立即独立重跑后通过;该不稳定性仍需纳入后续人工 soak,不能当作已完全消除。 + +最近一次断线回归还验证了独立 composer 在 `error` 终态后解除锁定,并通过受控 terminal event 显示可重试错误;该事件只属于 renderer 内部业务结果,不扩展 preload 或插件公共面。 + +上述证据覆盖 C0–C7 的阶段退出条件。最终逐项审计与 Windows 矩阵已经完成;未来能力只有在同一变更中具备 Definition、Provider、真实 Consumer、owner teardown 和真实终态测试时才可扩展该公共面。 diff --git a/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-vd-roadmap-review.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-vd-roadmap-review.md new file mode 100644 index 000000000..f12342d4e --- /dev/null +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/chat-kernel-vd-roadmap-review.md @@ -0,0 +1,200 @@ +# Chat Kernel vD0–vD7 对抗式审查与开发记录 + +> 归档说明(2026-08-21):本文是按时间追加的审查日志,仅用于追溯决策和历史证据,不再声明当前阶段状态。当前状态以 `../../chat-kernel-vd7-final-audit.md` 为准。 + +> 本文记录深度解耦路线的当前审查结论、退出条件和实现决策。它只记录本项目的代码与测试证据;DeepSeek Harness 文档用于审查标准,不替代本项目的真实测试。 + +## 1. 审查结论 + +### 1.1 不能把静态消费者报告当成真实消费者证明 + +`check-chat-kernel-consumers.mjs` 能证明生产源码中存在引用,不能证明 Electron 启动后该调用路径已连接到真实 producer,也不能证明终态、取消或 dispose 的行为。删除 facade 前必须同时具备静态零引用、动态入口 smoke 和行为测试。 + +### 1.2 不能把单元测试数量当成退出条件 + +当前 `test:chat-kernel` 为 138/138,`test:ui-system` 为 87/87,说明纯 kernel 与 UI controller 契约有证据;它们不能单独证明 renderer composition、preload 订阅、Electron 窗口恢复或真实 DOM 生命周期。vD5/vD6 的 PASS 还依赖 renderer responsibility gate、facade ledger、真实 Electron UI/主聊天/辅助恢复和 60-cycle lifecycle;vD7 仍不能由测试数量证明。 + +### 1.3 “拆文件”不等于“拆 owner” + +历史上 `renderer.js` 同时持有选择、topic、history、发送状态、TTS 队列、设置/主题、preload 事件和主 Surface 组装。当前这些责任已迁入 named owner 或现有 manager;D5 的验收仍以 owner 和可观察关系为准,而不是文件行数或新建模块数量。 + +### 1.4 terminal authority 的表述需要收紧 + +Coordinator 已负责 transport/persistence 终态,但 `StreamManager` 仍拥有 DOM projection 的 active、queue、pending 和 finalized 状态。路线不得把“唯一 terminal authority”写成“全仓只有一个状态对象”;应分别证明 transport terminal、durable commit、Surface projection terminal 三者的责任和顺序。 + +### 1.5 历史 Electron 记录不能覆盖当前失败 + +主聊天 20-run/489-action/173-request 和旧 lifecycle 记录是历史证据。当前分支必须重新运行真实入口;失败、超时、主进程 stderr 和子进程清理结果要分别记录,不能因存在旧的通过报告而标记 D7 完成。 + +## 2. vD0–vD7 修订路线 + +| 阶段 | 当前目标 | 必须证明的事实 | 禁止用作替代的证据 | +| --- | --- | --- | --- | +| vD0 | 冻结当前 owner、consumer、失败入口和基线 | report、动态入口、当前失败复现、未提交工作树边界齐全 | 仅看文件名或历史 commit | +| vD1 | 保持纯 StreamSession/State 协议 | 无 DOM/Electron;terminal、generation、迟到事件和 subscriber 隔离可复现 | DOM fixture 伪装成 kernel 证据 | +| vD2 | 保持 Coordinator 的 transport/persistence 顺序 | 每次 terminal 等待真实 reader 与 persistence Promise;dispose 达到 quiescence | `reader.close()` 或 `whenIdle()` 作为完成证明 | +| vD3 | 证明每个事件有真实 consumer | 主聊天、独立 Surface、辅助窗口各自消费并隔离 operation identity | 仅 producer/consumer 静态计数 | +| vD4 | 收口 RenderDependencies 与 Surface capability | MessageRenderer 只读显式 root、render model 和 capability closure;缺 capability fail-fast | 隐藏 global/ref fallback | +| vD5 | 把 renderer.js 变成 composition root | 只负责 DOM bindings、provider 构造、adapter mount/dispose;业务状态和副作用由 named owner 持有 | 以拆出几个文件或减少行数宣称完成 | +| vD6 | 退休所有无消费者 legacy surface | 每个保留出口有生产消费者、动态 smoke、期限和 owner;否则删除 | 测试夹具引用或导出本身自证 | +| vD7 | 最终边界与发布证明 | 单一 terminal/persistence authority、完整 Windows 矩阵、辅助窗口恢复、30–60 分钟人工 soak、逐项审计 | 旧报告、单元绿灯或短时压力测试 | + +## 3. vD5 施工顺序 + +1. **vD5.0 启动闭包**:修复 `initAudioContext` 作用域/初始化顺序;所有 capability 在 mount 前构造,缺失时 fail-fast;Electron UI smoke 必须先恢复。 +2. **vD5.1 状态 owner**:把主聊天 selection/topic/history、发送/取消 operation、TTS session/queue 分别交给 named owner;禁止 renderer 通过大量 getter/setter ref 继续充当隐式 Store。 +3. **vD5.2 事件 owner**:把 preload subscription、Flowlock、VCP log/group-topic、history watcher、notification side effect 迁入 `MainChatEventBridge` 或更窄的真实 consumer。 +4. **vD5.3 DOM adapter**:保留固定 DOM 查询和组合,移除 renderer 中的业务分支;统一主 Surface 与 auxiliary Surface 的 capability 创建路径,避免两套 RenderDependencies 语义。 +5. **vD5.4 legacy retirement**:逐项删除 `window.ensureAudioContext`、`window.showForwardModal` 和其它无消费者出口;每次删除同时更新 report、动态 smoke 和行为测试。 + +## 4. 决策记录 + +### 决策 vD-001:先修启动闭包,再迁移 owner + +**原因**:当前 Electron smoke 在 composition 阶段因 `initAudioContext` 未定义失败。继续迁移会把启动失败与结构变化混在一起,无法判断回归来源。 + +**结果**:第一项代码变更只修复 TTS capability 的作用域和占位覆盖,不改变聊天协议或用户行为;修复后必须重跑真实 Electron smoke。 + +**验证结果(2026-08-20)**:`node --check renderer.js` 通过;8 个主聊天 adapter/event/capability focused tests 通过;`npm run test:electron-ui-apps` 通过,24/24 场景通过。该修复解除 Electron UI smoke 的启动阻塞,但不代表 vD5 或 vD7 完成。 + +### 决策 vD-002:renderer.js 的 Definition of Done 是责任约束 + +**原因**:减少文件大小不能证明状态和副作用已经迁移。 + +**结果**:D5 完成判据改为可观察 owner 关系:renderer 不写 history、不拥有 stream/TTS 队列、不处理 preload 业务事件,只组装并 dispose adapter。 + +### 决策 vD-003:保留历史证据,但禁止冒充当前状态 + +**原因**:旧的 20-run、lifecycle 和 Electron UI 报告仍有追溯价值,但当前分支已出现新的启动失败。 + +**结果**:文档将历史运行与当前复核分开记录;当前失败必须有命令、退出码、错误位置和恢复状态。 + +## 5. 当前基线与未决问题 + +- 当前静态门禁:`guard:chat-kernel-consumers`、`guard:next-delta`、renderer responsibility 与 retired-global gates 通过。 +- 当前纯测试:Chat Kernel 138/138、UI System 87/87 通过。 +- 当前真实入口基线:Electron UI Apps 24/24、主聊天 24 actions / 25 requests、辅助 crash recovery 24 actions / 27 requests、60-cycle lifecycle 和当前主机六行矩阵通过。vD5/vD6 已 exit-ready;人工 soak 与跨配置 Windows 矩阵仍未完成。 +- 当前已知恢复风险:VoiceChat/Rust Assistant 在最终 topic 创建前进程异常可能丢失短会话,需要在 vD7 soak/recovery 中验证或制定产品策略。 +- 当前工作树的 `styles/themes.css` 修改与 `audio_engine/AppData/` 未跟踪内容不属于本路线变更,开发时必须保持隔离。 + +2026-08-20 vD5.0 复核(历史):TTS capability 修复后,`npm run test:electron-ui-apps` 通过 24/24;`npm run test:electron-lifecycle-stress` 通过 3 次预热 + 20 次测量,documents、connected DOM、detached DOM、listeners、scopes 和 owner resources 均稳定。该记录已被后续 60-cycle 复核扩展,不能单独作为当前 D7 证据。 + +### 决策 vD-004:TTS 由 Surface owner 持有 + +**原因**:AudioContext、播放源、队列、session identity 和 preload subscriptions 原先是 renderer 全局可变状态,停止与异步 decode 之间存在迟到结果重新播放的风险。 + +**结果(2026-08-20,历史增量)**:`TtsSurfaceOwner` 已接管生产订阅、session replacement、播放源停止、迟到 decode 隔离和异步 dispose;`window.ensureAudioContext` 已从生产组装删除。该增量当时为 115/115;当前 Chat Kernel 为 129/129。TTS 旧函数体已删除,renderer 中仍有其它状态和事件 owner 待迁移,因此 vD5.1 仍未完成。 + +### 决策 vD-005:删除无生产消费者的 renderer facade + +**原因**:`window.showForwardModal` 与 `window.setChatPresentationMode` 只有定义,没有生产读取;继续保留会让 consumer report 误把兼容入口当成公共合同。 + +**结果(2026-08-20)**:两个 facade 已删除;forward 通过 Surface capability,presentation 更新通过 `window.applyChatPresentationMode` 的现有真实消费者。consumer gate 通过,Electron UI Apps 24/24 通过。`window.globalSettings`、主题和过滤入口仍有真实生产消费者,暂不删除。 + +### 决策 vD-006:所有 renderer preload 订阅必须进入同一 teardown owner + +**原因**:`onReloadAgentSettings` 与 `onHistoryFileUpdated` 原先直接注册,虽然聊天内容测试通过,但 renderer dispose 不能证明这些监听器已经撤销;迟到 watcher 可能重新写入已替换的 Surface。 + +**结果(2026-08-20,进行中)**:两类订阅已使用 `ownedRendererSubscriptions` 管理并等待异步 dispose;Loom、VCP log/status 也已由 `MainChatAuxiliaryEventOwner` 接管。Flowlock command/request 已移入 `MainChatFlowlockOwner`,并覆盖未知命令、缺失 manager、响应失败和迟到回调隔离;forward modal、目标选择、原始消息读取和附件转发已移入 `ForwardMessageOwner`;`MainChatSettingsOwner` 现持有主窗口 settings 对象并提供稳定 ref、replace/update/snapshot/dispose,避免设置加载替换局部 owner。剩余 document/window listeners 仍待迁移,两个新 owner 仍需真实 Electron 矩阵覆盖。主聊天 24-action、lifecycle stress 和 Electron UI smoke 保持通过。 + +**vD5.4 清理(2026-08-20)**:已从 `renderer.js` 物理删除迁移后的 Flowlock 与 forward 内联实现;源码中不再保留 `messageToForward`、`selectedForwardTarget` 或旧 `flowlockCommandHandler`。consumer guard 与主聊天 24-action 序列复跑通过。剩余 listener owner 化及辅助窗口长矩阵仍未完成。 + +**vD5.3 listener 增量(2026-08-20)**:新增 `DomListenerOwner`,收口聊天 presentation quick switcher 及字体/布局/用户气泡设置控件的 DOM listener 注册,并在 renderer capability dispose 时反注册。owner 的移除和迟到 add 行为有 focused test;Electron UI Apps 24/24 通过。DOMContentLoaded、主聊天按钮 wiring、Rust 设置控件和 `setupEventListeners` 仍需后续逐项审计。 + +本轮追加:主聊天 emoticon trigger 与 quick-new-topic bridge listener 也纳入 `DomListenerOwner`;主聊天 24-action 序列复跑通过。`setupEventListeners` 模块仍有大量直接注册,不能把当前局部收口外推为全部 listener 完成。 + +**主聊天事件 owner 增量(2026-08-20)**:`setupEventListeners` 现在接收显式 `listenerOwner`,消息区点击、发送按钮 click/contextmenu、输入框 keydown/input/mousedown 和附件按钮 click 均通过 owner 注册并可 teardown;主聊天 24-action 与 Electron UI Apps 24/24 通过。模块其余设置/通知/Rust listeners 仍待迁移。 + +**模块 listener 捕获增量(2026-08-20)**:`DomListenerOwner.capture()` 在 `setupEventListeners` 同步组装期间临时捕获模块直接调用的 `EventTarget.addEventListener`,函数结束立即恢复原型,并由 owner 记录反注册;focused capture test、主聊天 24-action 和 Electron UI Apps 24/24 通过。该机制不替代逐项 consumer 审计,异步/运行期注册和 MutationObserver 仍需明确 owner。 + +**异步 DOM 资源增量(2026-08-20)**:renderer 的 DOMContentLoaded 注册、emoticon trigger observer 与 quick-new-topic observer 已归入 `DomListenerOwner`;`own()` 支持 observer/disposable teardown,并有幂等 focused test。主聊天 24-action 复跑通过。`setupEventListeners` 内的 timer 和运行期临时 listener 仍需独立清理审计。 + +**timer/临时 listener 增量(2026-08-20)**:`DomListenerOwner.timeout()` 现管理 `setupEventListeners` 的滚动条隐藏、设置导航动画、上下文菜单延迟注册、助手长按与侧栏长按 timer;运行期 document click 通过显式 owner 注册。dispose 会取消 pending timeout,focused cancellation test 与主聊天 24-action 通过。 + +**主题/presentation owner 增量(2026-08-20)**:新增 `MainChatThemeOwner`,接管初始 light/dark 主题投影、presentation mode 规范化、DOM class、settings authority 更新、pretext/layout 协调、持久化失败回滚与 dispose 后隔离。renderer 保留的同名函数仅为 composition/compatibility 委托。focused rollback tests、主聊天 24-action 和 Electron UI Apps 24/24 通过;appearance-studio 仍通过现有 facade 消费,尚不能删除 facade。 + +**辅助窗口恢复增量(2026-08-20)**:主聊天真实序列新增 VoiceChat 与 Rust Assistant 各自的 held-stream reload 场景;释放迟到结果后重新加载的辅助 renderer 必须恢复可输入且无 thinking/streaming 残留。串行真实序列通过,当前为 24 actions / 25 VCP requests。该证据仍不是完整 crash/reload 长矩阵或人工 soak。 + +**Crash 矩阵结果(2026-08-20)**:首次 opt-in 运行暴露了真实产品缺陷:VoiceChat/Rust Assistant 的主进程窗口 owner 在 `render-process-gone` 后仍保留坏 BrowserWindow,导致 reopen 复用失效 renderer。已在 `voiceHandlers.js` 与 `assistantHandlers.js` 释放 owner 并销毁坏窗口;随后 `VCPCHAT_AUX_CRASH_MATRIX=1` 真实序列通过,24 actions / 27 VCP requests,覆盖两类辅助窗口 crash 后重建、可输入状态和无 transient thinking/streaming 残留。该证据仍是单次 opt-in,不等同于完整长时间 crash soak。 + +**生命周期压力结果(2026-08-20)**:当前代码复跑 `VCPCHAT_STRESS_CYCLES=60 npm run test:electron-lifecycle-stress` 通过(3 warmup + 60 measured)。baseline 到 cycle-60 保持 connectedElements=2569、listeners=875、detachedRoots=0、lifecycleActiveResources=163、processes=5;人工 soak 仍未完成。 + +**Windows 入口复核(2026-08-20)**:`node scripts/test-settings-wa-electron.mjs` 通过 1–7 项(仅窗口 resize CDP 能力跳过);`node scripts/test-next-ui-tab-lifecycle.mjs` 通过;Electron UI Apps 24/24 通过。旧 `test-top-tab-session.mjs` 直接 eval 旧 facade、未组装 `VCPNextShellController`,因此不能作为当前产品 tab reload 证据,已由真实 tab lifecycle 入口替代并记录。 + +并行 stability 尝试曾因同时启动多个 Electron 进程触发 CDP `Runtime.callFunctionOn` timeout;串行主聊天序列随后通过 24 actions / 25 requests。并行 timeout 不计为源码回归,也不覆盖人工 soak 缺口。 + +**当前证据汇总(2026-08-20)**:Chat Kernel 129/129、UI System 85/85、Electron UI Apps 24/24;生命周期压力为 3 次预热 + 60 次测量;辅助窗口 reload/crash opt-in 三轮合计 72 actions / 42 requests;设置入口 1–7 与真实 Next UI tab lifecycle 通过。完整 Windows 配置矩阵、30–60 分钟人工 soak 和 D7 逐项审计仍未完成。 + +**vD5 listener 审计增量(2026-08-20)**:对抗式复核发现 `filterManager.subscribe`、`onDoToggleNotificationsSidebar` 和 `onCreateUnlockedTopic` 原先没有把 disposer 交给 renderer owner,现已通过 `listenerOwner.own(...)` 纳入同一 teardown。`setupEventListeners` 的临时 `EventTarget` 捕获改为 `try/finally`;异常时释放原型捕获并撤销 `eventListenersBound`,允许安全重试。`window.globalSettings`、presentation mode helpers 与 `checkMessageFilter` 仍有真实生产消费者,保留为兼容桥并列入 D5/D6 责任表,不以静态定义删除。 + +**vD5.3 input enhancer 增量(2026-08-20)**:`inputEnhancer` 的拖拽、粘贴、提及建议和跨窗口附件回调现在接受主 renderer 的 `DomListenerOwner`;重初始化会先撤销旧资源,跨窗口 preload disposer 也进入同一 owner。focused owner test 与 `npm run test:electron-ui-apps`(24/24)已通过。TopicListManager 仍有独立的动态列表 listener,需要在后续 D5/D6 审计中证明其宿主生命周期,不能把 input enhancer 的迁移外推为全部 DOM listener 完成。 + +**vD5.3 topic-list 增量(2026-08-20)**:TopicListManager 现在接受独立的 `topicListDomListenerOwner`;Next UI 搜索/管理工具与话题区快捷键通过该 owner 注册,渐进渲染的 topic item listener 在每次重绘前撤销,observer、Sortable、滚动监听和上下文菜单由 manager dispose 清理。`topic-list-mode-lifecycle.test.js` 两项通过,`guard:next-delta` 已更新共享基线 hash/rationale。TopicListManager 仍直接读取若干 legacy UI services(例如 `window.uiManager`),这些是业务兼容消费者,尚未纳入 D7 删除范围。 + +**vD5.3 timer 审计增量(2026-08-20)**:`modules/renderer/enhancedColorUtils.js` 原先在模块加载时安装每小时 `setInterval`,没有 renderer owner,可能让进程生命周期超出 Surface dispose。已删除该常驻 interval,TTL 缓存改为按访问清理;focused lifecycle test 验证模块加载不创建 process-lifetime interval。该变更不改变颜色提取或缓存结果。 + +**当前真实入口复核(2026-08-20)**:TopicListManager 与 enhancedColorUtils 变更后,串行 `npm run test:electron-main-chat-sequences` 通过(24 actions / 25 VCP requests);`VCPCHAT_STRESS_CYCLES=60 npm run test:electron-lifecycle-stress` 通过(3 warmup + 60 measured)。baseline 到 cycle-60 保持 connectedElements=2569、listeners=875、detachedRoots=0、lifecycleActiveResources=163,heap 约 10.3–10.4 MiB。该结果加强了生命周期证据,但仍不是 30–60 分钟人工 soak 或完整 Windows 配置矩阵。 + +**Windows 矩阵入口增量(2026-08-20)**:新增 `npm run test:windows-matrix`,串行运行 UI/Classic 24 项、Settings WA、Next tab lifecycle、主聊天默认、辅助 crash recovery 和 60-cycle lifecycle,并将每项命令、环境、退出码和截断输出写入 `artifacts/windows-matrix/.json`。当前 Windows 主机 `FlowX13` 的最新矩阵六项全部通过;Settings WA 的旧第 8 项断言曾在 `uiMode=next` 后等待 SettingsShell 消失,已修正为验证 canonical Next SettingsShell 在 reload 后保持。窗口 resize CDP 能力仍明确记录为 skipped,不伪装成通过。该入口覆盖当前主机,不等同于多版本 Windows/打包安装矩阵,也不替代人工 soak。 + +## 6. 开发记录规则 + +每个非机械代码变更必须在本文件或对应架构文档记录:问题复现、owner 选择、被放弃的替代方案、静态/单元/真实入口验证命令及结果。测试失败只能标记为失败或阻塞,不能用 agent 判断、旧报告或“理论上等价”替代源码和运行证据。 + +## 7. 人工 soak 执行入口(vD7 未完成) + +`npm run test:manual-soak` 启动 hermetic Electron 主窗口并按固定间隔记录 renderer lifecycle、heap、进程数、页面错误和 stderr。可通过 `VCPCHAT_MANUAL_SOAK_MINUTES` 设置 30–60 分钟时长;stdin 输入 `finish`、`fail` 或 `abort` 可提前结束。每次运行写入 `artifacts/manual-soak/.json`,状态恒为 `manual_observation_required`,不会自动声明通过。 + +操作员必须在运行期间覆盖:主聊天发送/流式/取消/重试、历史与话题切换、附件、主题、通知和桌面 push、VoiceChat、Rust Assistant、reload/crash recovery、Classic 子页面及插件协议,并观察 detached roots、listeners、heap 斜率和错误日志。当前尚无人工填写的通过记录,因此 vD7 仍保持未完成;单台 Windows 矩阵也不能代表多版本、打包安装或 GPU/DPI 矩阵。 + +逐项退出审计固定在 [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md)。当前审计将 vD5/vD6 标记为 PASS、vD7 标记为 BLOCKED;审计文档本身不替代运行证据。 + +**vD5.3 settings capability 增量(2026-08-20)**:`global-settings-manager` 保存路径现在优先接收 composition root 注入的 `normalizeChatPresentationMode`、`applyChatPresentationMode`、`applyChatBubbleLayoutSettings` 和 appearance provider,不再把主 renderer 的 presentation owner 当作隐式业务依赖;旧 window bridge 仅作为独立旧测试/兼容环境的 fallback。`tests/global-settings-save.test.mjs`、`node --check modules/global-settings-manager.js` 和 `node --check renderer.js` 通过。该增量减少 ambient DOM/global 依赖,但不代表 renderer 已完成 composition-only 收口。 + +**vD5.3 command capability 增量(2026-08-20)**:`MainChatCommands` 增加 `configureCapabilities()`,由 renderer composition root 注入 ui helper、UI manager、item list、filter、notification、appearance studio 和 top-tab capabilities;命令内部优先使用注入对象,window facade 仅作为兼容 fallback。`node --check modules/mainChatCommands.js`、`node --check renderer.js`、`guard:chat-kernel-consumers` 与 `guard:next-delta` 通过。Next shell 及 Classic 入口仍通过既有 `window.MainChatCommands` command facade,故未删除该公共 facade;剩余 `notificationRenderer`、appearance-studio 和 UI manager 内部 ambient 读取继续按真实消费者审计。 + +**vD5.3 notification lifecycle 增量(2026-08-20)**:`notificationRenderer` 增加 filter capability 注入和 listener-owner 配置;focus cleanup 与 30 秒过期清理优先由 `DomListenerOwner` 持有,避免生产 notification timer 脱离 renderer dispose。主聊天真实序列 24 actions / 25 requests、`node --check` 和 Next delta gate 通过。若独立旧页面未配置 owner,兼容 fallback 仍保留,因此该增量不等同于所有通知路径已完成 owner 化。 + +**vD5.3 UI manager lifecycle 增量(2026-08-20)**:`uiManager.init()` 接受 `listenerOwner`,使用 owner capture 收口 sidebar/navigation 的直接 DOM listener,并将数字时钟从常驻 `setInterval` 改为 owner-scoped recursive timeout;renderer composition root 传入主窗口 `DomListenerOwner`。`node --check modules/uiManager.js`、`node --check renderer.js`、`guard:next-delta` 和主聊天 Electron 24-action 序列通过。UI manager 内部仍读取 settings/item-list globals 的真实服务,暂不删除,继续列入 D5/D6 审计。 + +**vD5.3 UI manager capability 增量(2026-08-20)**:UI manager 现在由 renderer composition root 注入 `settingsManager` 与 `itemListManager`,settings reload/display、mouse-state reset 和 unread badge 更新优先走显式 capability;window service 仅作为旧页面 fallback。`node --check`、两项 consumer/Next gate 和主聊天 24-action 序列通过。该迁移不改变 UI manager 对外 facade,避免破坏 Classic 子页面和插件协议。 + +**vD5.3 appearance capability 增量(2026-08-20)**:Appearance Studio 增加 `configureCapabilities()`,由 composition root 注入 settings、appearance engine、UI manager、presentation normalizer 和 presentation applier;主题/预览/回滚路径优先使用显式 capability,旧 window bridge 保留为独立旧页面 fallback。`test:appearance-studio`、主聊天 24-action、`node --check` 和 `guard:next-delta` 通过。Appearance Studio 的公共 window facade 仍有 Next shell 与设置 UI 真实消费者,不能删除。 + +本轮补充审计:Appearance Studio 的 settings、appearance revision、toast/modal 和 presentation 读取均改为通过 capability helper 优先解析,减少残余 ambient 访问;`test:appearance-studio` 与 `guard:next-delta` 复跑通过。剩余 window 访问仅作为旧页面兼容 fallback 或公共 facade 暴露,仍需最终 consumer report 逐项确认。 + +**完整 UI System 复核(2026-08-20)**:`npm run check:ui-system` 全链路通过,包含 design subtraction、Classic parity/retirement、Next delta、Chat Kernel consumer、UI application/page runtime、stylelint、VCPUI consumer gate、UI System 85/85、appearance engine/studio、Next tab lifecycle、WebAwesome adapter 和 lifecycle/topic tests。该复核证明当前 UI 组合与静态边界稳定,但不替代 D7 人工 soak 或多主机 Windows 矩阵。 + +**vD5.3 UI helper capability 增量(2026-08-20)**:UI manager 的设置标签中键打开 modal 路径现优先使用 renderer 注入的 `uiHelper`,window helper 仅保留兼容 fallback;`node --check`、两项静态 gate 和主聊天 24-action 序列通过。 + +**vD5.3 UI manager dispose 增量(2026-08-20)**:UI manager 增加幂等 `dispose()`,撤销 capability 引用和 DOM 元素引用;renderer 在初始化后把该 disposer 注册到 `ownedRendererSubscriptions`,与 `DomListenerOwner` 一起完成主 Surface teardown。重复 dispose 安全,dispose 后重新 init 明确失败;`node --check`、静态 gates 和主聊天 24-action 序列通过。 + +**vD7 soak harness cleanup 增量(2026-08-20)**:`test-electron-manual-soak.mjs` 现在在 Windows 使用精确 PID 的 `taskkill /T /F` 终止 Electron 子进程树,并等待父进程退出;Linux/macOS 使用 SIGTERM/SIGKILL fallback。短时真实启动复核成功生成 observation artifact,仍固定为 `manual_observation_required`,不构成人工 soak 通过。 + +### 决策 vD-007:内部 ambient globals 全部迁移,公共 facade 分层保留 + +**原因**:把所有 `window.*` 一次性删除会破坏 Next、Classic 或插件公共命令协议;继续保留可变 settings/filter/presentation 全局又会形成第二状态权威。 + +**结果(2026-08-21)**:停止发布并删除 `window.globalSettings`、`window.applyChatPresentationMode`、`window.normalizeChatPresentationMode`、`window.checkMessageFilter` 和 `window.applyChatBubbleLayoutSettings`。内部消费者改用 settings snapshot/accessor、message-filter evaluator 和 presentation normalize/apply capability,写操作继续由既有 manager 持有。`window.VCPMainChatState`、`window.MainChatCommands` 与 `window.VCPAppearanceStudio` 作为公共 facade 保留,value 冻结且 window property 不可写、不可配置;测试通过 contribution registry/test adapter 覆盖,不再替换 facade。consumer report 同时清点 164 个 Classic/feature-local ambient 名称;它们是显式兼容债务,不属于本决策已经退休的目标集合。 + +### 决策 vD-008:selection 完成必须包含 topic projection consumer + +**原因**:最终真实 Electron 序列发现 reload 后 durable selection 和 history 已恢复,但 `topic-item.active` 尚未投影;`selectItem()` 启动 `loadTopicList()` 后直接返回,producer commit 与 consumer completion 没有同一完成语义。 + +**结果(2026-08-21)**:`ChatManager.selectItem()` 等待 `topicListManager.loadTopicList()`,随后重新校验 selection generation,再持久化并返回。focused regression 证明 startup restore 不会早于 topic projection;主聊天 24 actions / 25 requests 与最终 Windows 矩阵通过。 + +### vD5/vD6 最终证据与限制 + +2026-08-21 最终当前主机矩阵 `artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json` 六行通过。Lifecycle 为 3 warmup + 60 measured,connected elements 2569、listeners 876、processes 5、owner resources 163 稳定,detached roots/icons/options 为 0。严格 D5 复核后新增 `MainChatAttachmentOwner` 与 `MainChatSendOwner`,移除 `renderer.js` 对附件数组、发送按钮 DOM/ARIA、interruptible message 选择、group/agent 中止分派和本地取消回退的所有权;selection/topic 统一读取 `MainChatStateAuthority` refs,owner teardown 按注册逆序等待。`VCPMainChatState` 同时纳入 facade ledger,并以冻结、不可替换的只读 snapshot facade 保留给 Flowlock/AutoTTS。Chat Kernel 最终为 138/138,UI System 为 87/87。 + +矩阵 `2026-08-21T05-51-24-115Z.json` 和较早的 `2026-08-21T05-10-03-018Z.json` 均在主聊天行发生 Puppeteer `Runtime.callFunctionOn` protocol timeout,未产生产品断言或失败快照;同一最终代码的独立主聊天 sequence 与随后完整六行矩阵通过。失败 artifact 保留为测试基础设施波动证据,不能删除或用于证明产品 PASS。 + +vD5、vD6 标记 PASS;vD7 继续 BLOCKED。尚缺 30-60 分钟人工 soak checklist,以及支持范围内多 Windows 版本、打包方式、GPU/DPI 组合证据。 + +### 2026-08-21 对抗式补救复核 + +复核暴露并修复五类边界缺陷:selection authority 冻结值仍被原地修改;`VCPMainChatState` snapshot 嵌套对象可变且暴露 history ref;settings/presentation owner 未达到 quiescent dispose;attachment ref 存在外部数组别名与 durable-save 竞态;facade gate 只检查 smoke 文件存在。修复采用 clone-and-set authority、递归克隆冻结 consumer snapshot、tracked async generation guard、copy/freeze/append attachment owner,以及 npm smoke 入口和具体协议断言绑定。focused regression 33/33、Chat Kernel 140/140、UI System 88/88、完整 `check:ui-system`、consumer/Next gates、主聊天 24 actions / 25 requests 均通过。本补丁 lifecycle 复跑为 3 warmup + 20 measured,资源计数稳定且 detached roots/icons/options 为 0;历史 60-cycle 证据仍保留,但不冒充本补丁重跑。 + +**UI manager lifecycle focused test(2026-08-20)**:新增 `tests/ui-manager-lifecycle.test.mjs`,真实加载 UI manager facade,验证初始化后 `dispose()` 幂等、capability/DOM 引用撤销且 dispose 后重新 init 明确拒绝;该测试已纳入 `test:ui-system`。 + +**当前 Windows 矩阵复核(2026-08-20 20:49Z)**:`npm run test:windows-matrix` 在 FlowX13 / win32 / x64 / Node 22.17.1 上六项全部通过:UI/Classic 24/24、Settings WA、Next tab lifecycle、主聊天 24 actions / 25 requests、辅助 crash recovery 24 actions / 27 requests、lifecycle stress 3 warmup + 60 measured。最新 artifact 为 `artifacts/windows-matrix/2026-08-20T20-49-30-813Z.json`。Settings WA 的 resize CDP 能力仍按 skipped 记录;该矩阵仍只覆盖当前主机,不代表多版本、打包安装或 GPU/DPI 组合。 diff --git a/docs/next-ui-webawesome-roadmap.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/next-ui-webawesome-roadmap.md similarity index 97% rename from docs/next-ui-webawesome-roadmap.md rename to docs/archive/2026-08-chat-kernel-and-ui-roadmaps/next-ui-webawesome-roadmap.md index ab0248aaf..fa287b74e 100644 --- a/docs/next-ui-webawesome-roadmap.md +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/next-ui-webawesome-roadmap.md @@ -1,5 +1,7 @@ # Next UI 与 Web Awesome 低侵入建设路线 +> 归档说明(2026-08-21):并行 presentation 建设阶段已经结束。当前产品拓扑见 `../../next-ui-current-state.md`,后续顺序见 `../../next-ui-development-roadmap.md`。 + > 状态:历史建设方案。当前 Web Awesome 已固定为 VCPUI 背后的离线可替换内核;真实完成度和后续减法分别见 [`next-ui-current-state.md`](./next-ui-current-state.md) 与 [`next-ui-development-roadmap.md`](./next-ui-development-roadmap.md)。 ## 目标 diff --git a/docs/ui-applications-webawesome-migration-plan.md b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/ui-applications-webawesome-migration-plan.md similarity index 99% rename from docs/ui-applications-webawesome-migration-plan.md rename to docs/archive/2026-08-chat-kernel-and-ui-roadmaps/ui-applications-webawesome-migration-plan.md index ca552e78f..5a4c8a289 100644 --- a/docs/ui-applications-webawesome-migration-plan.md +++ b/docs/archive/2026-08-chat-kernel-and-ui-roadmaps/ui-applications-webawesome-migration-plan.md @@ -1,5 +1,7 @@ # VCPChat 应用页面与全局设置 Web Awesome 路线图 +> 归档说明(2026-08-21):该整页迁移计划已停止执行。未来业务页面只允许按真实需求在独立 PR 中连同 runtime、consumer 和测试一起引入。 + > 状态:历史页面迁移方案,当前不实施。无生产消费者的业务子页面 Next runtime 已在 P2 删除;未来只允许页面、runtime 与测试在同一独立 PR 引入。 > 基线日期:2026-08-01 > 适用分支:`codex/vcpchat-codex-app-server` diff --git a/docs/chat-event-producer-consumer-roadmap.md b/docs/chat-event-producer-consumer-roadmap.md new file mode 100644 index 000000000..267f0f5c5 --- /dev/null +++ b/docs/chat-event-producer-consumer-roadmap.md @@ -0,0 +1,218 @@ +# Chat Event Producer/Consumer 类型化路线 + +更新日期:2026-08-21。本文件是 D0-D7 之后的独立开发计划,不是当前 D7 完成声明,也不要求在现有 PR 中重写全部事件系统。 + +## 目标 + +把 VCPChat 的聊天相关事件从“可搜索的字符串和回调集合”提升为可审计的事件定义与 producer/consumer 图谱。每个纳入范围的事件最终都应能回答:谁产生、谁消费、传递模式是什么、payload 如何校验、是否是持久事实、terminal 如何定义、何时丢弃、由谁撤销订阅。 + +这项工作服务于三个目的: + +1. 防止 renderer、ChatManager、StreamManager、preload 和 Surface 再次形成隐式反向控制。 +2. 让新的聊天能力优先接入显式 capability 或已登记事件,而不是继续增加 `window.*` 或裸字符串。 +3. 生成可审查的 producer/consumer 报告,并把关键错误路径接入静态门禁和真实入口测试。 + +## 不改变的边界 + +- 不把所有 Electron IPC channel、DOM 原生事件、Classic 页面协议强行改成同一个运行时 EventBus。 +- 不在第一阶段替换现有 `electronAPI`、`contextBridge`、插件协议或 `CustomEvent` 的传输实现。 +- 不用类型包装掩盖未解决的 owner、持久化或 terminal authority 问题。 +- 不把测试 mock 中的事件当成生产 consumer;生产、测试、兼容协议三类证据必须分开。 +- 不增加第二份聊天状态。事件只传播事实或请求,当前 snapshot 和 mutation authority 仍由现有 owner 持有。 + +## 现状分类 + +当前事件面至少分为四类,迁移顺序不能混淆: + +| 类别 | 当前例子 | 主要 owner | 第一阶段处理 | +| --- | --- | --- | --- | +| DOM/UI 事件 | `vcp-renderer-ready`、`vcp-chat-stream-terminal`、`next-ui-overlay-changed` | Surface/DOM owner | 登记 payload、producer、consumer 和 dispose;暂不换传输 | +| preload/Electron subscription | `onHistoryFileUpdated`、`onThemeUpdated`、`onVCPLogStatus`、`onFlowlockCommand` | preload capability + named owner | 登记 channel、参数、返回 disposer、迟到结果规则 | +| renderer 内部事件/回调 | Stream consumer、ChatSurface slots、presentation listeners | kernel/surface owner | 优先改为显式 typed callback 或 capability;禁止新增 ambient callback | +| 公共兼容协议 | `MainChatCommands`、`VCPAppearanceStudio`、Classic/plugin hooks | 对外协议 owner | 固定 API、冻结对象、登记版本和退役条件;不作为内部事件总线 | + +原生 DOM `click`、`input`、键盘事件和非聊天窗口 IPC 不在首批图谱范围,除非它们直接改变聊天 durable state 或 stream terminal。 + +## 目标事件定义 + +首个版本使用 JavaScript/JSON 可生成的定义模型,不要求立即引入 TypeScript 全仓迁移。定义文件建议放在 `scripts/chat-event-definitions.json`,生成报告放在 `docs/chat-event-producer-consumer.json`。 + +每条事件定义包含以下字段: + +```json +{ + "id": "chat/stream/chunk", + "transport": "internal-callback", + "mode": "emit", + "payload": { + "required": ["conversationKey", "operationId", "messageId", "textDelta"], + "optional": ["sequence"], + "opaqueIds": ["conversationKey", "operationId", "messageId"] + }, + "producer": [{"owner": "StreamCoordinator", "source": "modules/chat/streamCoordinator.js"}], + "consumers": [{"owner": "StreamProjection", "source": "modules/renderer/streamProjection.js"}], + "durability": "transient", + "terminal": {"kind": "none", "ordering": "sequence-monotonic"}, + "discard": "drop-after-dispose-or-generation-mismatch", + "lifecycle": {"subscriptionDisposer": true, "quiescentDispose": true}, + "smoke": "test:electron-main-chat-sequences", + "status": "active" +} +``` + +字段语义必须固定: + +- `id` 是稳定的逻辑事件名,不等于 DOM event name 或 IPC channel。 +- `transport` 记录实际载体:`dom-custom-event`、`preload-subscription`、`internal-callback`、`ipc-command`、`public-protocol`。 +- `mode` 至少区分 `emit`、`request`、`waterfall`、`serial`、`parallel`;没有明确模式的事件先标 `unclassified`,不能假装已类型化。 +- `durability` 只能是 `durable-fact`、`transient`、`request` 或 `projection`。模型/历史可见事实不能标成纯 projection。 +- `terminal` 记录是否允许 terminal、terminal 集合、单 terminal 规则和顺序约束。 +- `discard` 记录 dispose、generation、conversation identity、selection 或 surface root 失效时的丢弃规则。 +- `lifecycle` 必须说明订阅 disposer、in-flight drain、回调异常隔离和 owner。 +- `producer` 与 `consumers` 必须包含源码位置和 owner;定义者不能把自己算作 consumer。 +- `smoke` 指向真实入口或 focused owner test,不接受只有静态测试文件存在的证据。 + +## 事件命名与类型策略 + +逻辑命名采用域/主题/动作三级形式,例如: + +- `chat/stream/started` +- `chat/stream/chunk` +- `chat/stream/terminal` +- `chat/history/selected` +- `chat/history/persisted` +- `chat/surface/disposed` +- `chat/presentation/changed` + +请求和事实必须分开命名:`chat/send/requested` 不能代替 `chat/message/persisted`。同一动作若既有 request 又有 durable fact,必须登记两条事件及其因果关系。 + +首阶段类型实现采用三层: + +1. JSON 定义负责跨文件登记、报告生成和静态字段完整性。 +2. `modules/chat/chatEventTypes.js` 导出冻结的 event id、mode、terminal kind 和 payload guard,供生产代码和测试使用。 +3. 各 owner 继续使用自己的 JavaScript API;只有跨 owner 的 payload 才通过 `publishChatEvent/subscribeChatEvent` 或已存在的 capability closure 传递。 + +不为同一进程内已由静态接口保证的值添加重复 hostile-input 校验;IPC、durable file、插件和公共协议边界必须校验。 + +## 分阶段计划 + +### E0:事件面冻结和基线 + +- 扫描 renderer、modules、preloads、main process、Classic、插件入口和测试,提取事件定义、订阅、发送、回调注册和 disposer。 +- 为每个候选事件标注 transport、owner、producer、consumer、生产/测试证据和是否属于聊天范围。 +- 建立 `scripts/check-chat-event-inventory.mjs`,先只报告未知项,不阻断既有开发。 +- 输出 `docs/chat-event-inventory.json`,把未分类事件单独列为债务,不将“未找到 consumer”直接解释为无效事件。 +- 记录当前可观察基线:IPC 次数/顺序、terminal、持久化、DOM/ARIA、监听器和资源计数。 + +退出条件:范围内事件 100% 有候选 owner,所有未知字符串都有文件和原因;没有删除或改变生产事件。 + +### E1:逻辑事件定义和 schema + +- 建立 `chat-event-definitions.json` 的 schema 和校验脚本。 +- 先录入高价值小集合:stream started/chunk/terminal、history selection/persistence、surface mount/dispose、settings/presentation changed。 +- 为 operation identity、conversation identity、message identity 建立 opaque-id 约束。 +- 为 terminal 事件验证单 terminal、terminal kind 和迟到 chunk 丢弃规则。 +- 生成 `docs/chat-event-producer-consumer.json`,报告定义、源码位置、生产消费者、测试消费者、smoke 和退役状态。 + +退出条件:高价值事件定义可校验、可生成报告,故意缺 producer/consumer/mode/terminal/disposer 的定义会失败。 + +### E2:内部 stream 事件接缝 + +- 以 `StreamCoordinator` → `StreamConsumer` 为第一条真实迁移链。 +- 让 started/chunk/terminal 使用统一 payload builder 和 operation identity;保留现有 coordinator 和 projection runtime,不迁移 DOM。 +- 明确 chunk 的 sequence、terminal arbitration、dispose、generation mismatch 和 consumer exception 规则。 +- 增加 focused tests:重复 terminal、过期 chunk、切换 conversation、consumer 抛错、dispose drain、超大/空 chunk。 +- 将 `check-chat-kernel-consumers` 扩展为检查定义与实现的一致性,而不是只检查正则。 + +退出条件:主聊天和独立 Surface 的真实 Electron sequence 通过,且没有新增全局状态或第二份 history。 + +### E3:history/persistence 事件 + +- 区分 `requested`、`optimistic-projected`、`persisted`、`persistence-failed` 和 `retracted`,不把一次保存过程压成一个模糊事件。 +- 将 persistence authority、mutation queue 和 ChatManager selection generation 写入定义。 +- 验证失败保存不会伪造 durable fact;迟到保存结果不能写入新 topic/conversation。 +- 对 durable facts 增加 reload/replay 测试,确认事件或事实可以重建用户可见历史。 + +退出条件:持久化成功、失败、取消、切换和 dispose 的 IPC/文件/DOM 差异都有解释,且 terminal/persistence authority 唯一。 + +### E4:preload IPC 与 DOM 适配层 + +- 为 `onHistoryFileUpdated`、`onThemeUpdated`、`onVCPLogStatus`、`onFlowlockCommand` 等聊天相关订阅登记逻辑事件 id 与实际 channel。 +- 在 preload capability 中提供 typed adapter:参数归一化、返回 disposer、channel 错误隔离;不把 `ipcRenderer` 暴露给 consumer。 +- 为 DOM `CustomEvent` 增加 typed dispatch/subscribe helper,只允许登记过的逻辑事件映射到 DOM name。 +- 所有 adapter 都记录 producer、consumer、owner、dispose 和 late-result 规则。 + +退出条件:生产代码不再新增裸 channel/裸 CustomEvent;现有 Classic/plugin 公共协议保持字节和调用顺序兼容。 + +### E5:生成式 producer/consumer 图谱 + +- 从定义文件和源码索引生成事件矩阵、按 owner 的入/出边、孤立 producer、无 consumer projection、无 owner subscription 和未登记 channel 报告。 +- 对动态注册的 consumer 允许显式 manifest,不允许通过宽泛正则自证。 +- 报告生产、测试、兼容协议三种消费者,避免测试引用掩盖生产无人消费。 +- 在 CI 接入 `guard:chat-event-producers`、`guard:chat-event-consumers` 和 `guard:chat-event-docs`。 +- 对每个事件要求最小负向控制:缺 disposer、错误 mode、错误 terminal、未知 payload 字段或错误 owner 必须在真实 runner 中失败。 + +退出条件:图谱可从干净 checkout 生成,定义/源码不一致会阻断 CI;报告能定位到源码行和真实 smoke。 + +### E6:公共协议和插件边界 + +- 将 `MainChatCommands`、`VCPAppearanceStudio`、Classic 子页面和插件命令登记为 `public-protocol`,而不是内部 event。 +- 为每个公共协议记录 API version、允许字段、冻结性、唯一 owner、兼容范围、动态 smoke 和退役条件。 +- 对协议输入在 preload/插件边界校验;内部同进程调用继续使用显式 capability,不反向依赖公共 facade。 +- 任何新增公共事件必须同时修改定义、consumer report、协议文档和真实 smoke。 + +退出条件:公共协议和内部事件没有混用;兼容 facade 不持有第二份状态或生命周期。 + +### E7:回归、发布和维护 + +- 完整运行 Chat Kernel、UI System、consumer/Next/design/Classic gates、Electron UI Apps、main-chat、auxiliary crash/reload、lifecycle stress 和 Windows matrix。 +- 对模型/用户可见事件增加 keyless replay 或 snapshot;对跨进程事件增加真实 Electron smoke。 +- 将事件定义变更纳入 PR 模板:新增事件、变更 payload、terminal、durability、owner、consumer、dispose、smoke 必须逐项说明。 +- 每个已退役事件保留 retired record,不复活旧 channel 或 facade;归档文档不能成为当前定义来源。 +- 每季度或每个重大协议变更后复核 producer/consumer 图谱,删除只剩历史证据的定义。 + +退出条件:定义、实现、测试、文档和生成报告一致;未解释的 IPC、持久化、terminal、DOM、ARIA、listener 或 resource 差异阻断发布。 + +## 测试矩阵 + +| 层级 | 证明内容 | 典型证据 | +| --- | --- | --- | +| schema/static | 字段完整、id 唯一、mode/transport 合法、源码引用已登记 | `guard:chat-event-*` | +| owner unit | payload、顺序、terminal、dispose、callback error | `tests/chat-event-*.test.js` | +| kernel integration | coordinator/session/history authority 真实协作 | `npm run test:chat-kernel` | +| UI integration | Surface、DOM adapter、ARIA/focus 和迟到结果 | `npm run test:ui-system` | +| Electron | preload channel、窗口 reload/crash、跨 Surface | `test:electron-main-chat-sequences`、`test:electron-ui-apps` | +| replay/snapshot | durable/model/user-visible 结果可重建 | 新增 keyless replay/snapshot | +| platform | Windows 版本、安装方式、GPU/DPI 和资源稳定性 | `test:windows-matrix`、人工 soak | + +测试必须验证外部世界:文件、IPC 顺序、DOM/ARIA、terminal、listener/resource 计数,而不是只断言某个函数被调用。 + +## 关键架构决策 + +1. 先定义逻辑事件,再绑定 transport。这样不会让 DOM 名称或 IPC channel 成为业务 API。 +2. 事件不是状态容器。当前 snapshot、selection、history 和 presentation 仍由唯一 authority 提供。 +3. durable fact、transient update、request 和 projection 必须分型,不能用一个 `event` 字段混合。 +4. 类型化优先从跨 owner、跨进程和持久化边界开始;同 owner 私有 callback 不为形式统一而包装。 +5. 先迁移真实消费者,再删除 ambient facade;静态零引用不能替代动态证据。 +6. 图谱生成失败应 fail closed,但未知事件在 E0-E1 只报告不阻断,避免一次扫描阻断正常修复工作。 + +## 风险和停止条件 + +- 如果定义引入第二份 stream/history 状态,立即停止并回退到现有 authority。 +- 如果 IPC 参数或 Classic/plugin 协议发生未登记变化,停止该切片并恢复 adapter 兼容层。 +- 如果 dispose 只发 abort 而不等待真实工作停稳,不得标记该阶段完成。 +- 如果事件图只能依赖宽泛 grep、测试文件存在或 producer 自证 consumer,不得接入强制 CI。 +- 如果真实 Electron sequence、Windows matrix 或 replay/snapshot 暴露未解释差异,不得宣称行为等价。 + +## 首个实施切片建议 + +首个 PR 只做 E0 + E1 的最小闭环: + +1. 新增事件盘点脚本和候选 inventory。 +2. 定义 5-8 个 stream/history/surface 高价值逻辑事件。 +3. 生成第一版 producer/consumer report。 +4. 增加 schema 和一个故意错误定义的负向测试。 +5. 不修改现有 IPC、DOM 事件和 StreamCoordinator 行为。 + +该切片完成后再以 E2 迁移 stream 事件。这样可以先验证 Harness 风格的“定义—生成—负向门禁—真实 smoke”闭环,再扩大事件范围,控制对主聊天行为的风险。 + diff --git a/docs/chat-kernel-consumer-report.json b/docs/chat-kernel-consumer-report.json new file mode 100644 index 000000000..e170ef3d4 --- /dev/null +++ b/docs/chat-kernel-consumer-report.json @@ -0,0 +1,15269 @@ +{ + "phase": "D6", + "ignoredProductionDirectoryNames": [ + ".git", + "docs", + "node_modules", + "screenshots", + "scripts", + "tests", + "vendor" + ], + "ignoredProductionDirectoryPaths": [ + "audio_engine/AppData" + ], + "productionFiles": [ + "Agenttaskmodules/task.html", + "Agenttaskmodules/task.js", + "AppData/LoomApps/example-mobile/inject.js", + "Canvasmodules/canvas.html", + "Canvasmodules/canvas.js", + "Desktopmodules/api/desktopMetrics.js", + "Desktopmodules/api/ipcBridge.js", + "Desktopmodules/api/vcpProxy.js", + "Desktopmodules/builtinWidgets/appTrayWidget.js", + "Desktopmodules/builtinWidgets/musicWidget.js", + "Desktopmodules/builtinWidgets/newsWidget.js", + "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "Desktopmodules/builtinWidgets/translateWidget.js", + "Desktopmodules/builtinWidgets/vchatApps.js", + "Desktopmodules/builtinWidgets/weatherWidget.js", + "Desktopmodules/core/dragSystem.js", + "Desktopmodules/core/performanceManager.js", + "Desktopmodules/core/state.js", + "Desktopmodules/core/statusIndicator.js", + "Desktopmodules/core/styleAutomation.js", + "Desktopmodules/core/theme.js", + "Desktopmodules/core/visibilityFreezer.js", + "Desktopmodules/core/wallpaperManager.js", + "Desktopmodules/core/widgetManager.js", + "Desktopmodules/core/zIndexManager.js", + "Desktopmodules/debug/debugTools.js", + "Desktopmodules/desktop.html", + "Desktopmodules/desktop.js", + "Desktopmodules/favorites/favoritesManager.js", + "Desktopmodules/favorites/thumbnail.js", + "Desktopmodules/ui/contextMenu.js", + "Desktopmodules/ui/dock.js", + "Desktopmodules/ui/globalSettings.js", + "Desktopmodules/ui/iconPicker.js", + "Desktopmodules/ui/saveModal.js", + "Desktopmodules/ui/sidebar.js", + "Dicemodules/dice.html", + "Dicemodules/dice.js", + "Flowlockmodules/flowlock-integration.js", + "Flowlockmodules/flowlock-protocol.js", + "Flowlockmodules/flowlock.js", + "Forummodules/forum.html", + "Forummodules/forum.js", + "Groupmodules/groupSettingsMarkup.js", + "Groupmodules/groupchat.js", + "Groupmodules/grouprenderer.js", + "Groupmodules/modes/baseChatMode.js", + "Groupmodules/modes/inviteOnlyMode.js", + "Groupmodules/modes/natureRandomMode.js", + "Groupmodules/modes/sequentialMode.js", + "Groupmodules/topicTitleManager.js", + "Logmodules/log.html", + "Logmodules/log.js", + "Loommodules/device-menu.html", + "Loommodules/manager.html", + "Loommodules/shell.html", + "Memomodules/memo-graph.js", + "Memomodules/memo-workbench.js", + "Memomodules/memo.html", + "Memomodules/memo.js", + "Musicmodules/music-effects.js", + "Musicmodules/music-lyrics.js", + "Musicmodules/music-output.js", + "Musicmodules/music-player.js", + "Musicmodules/music-sidebar.js", + "Musicmodules/music-ui.js", + "Musicmodules/music-utils.js", + "Musicmodules/music-visualizer.js", + "Musicmodules/music-webdav.js", + "Musicmodules/music.html", + "Musicmodules/music.js", + "Musicmodules/socket.io.min.js", + "Notemodules/notemini.html", + "Notemodules/notemini.js", + "Notemodules/notes.html", + "Notemodules/notes.js", + "PluginManagerModules/plugin-manager.html", + "PluginManagerModules/plugin-manager.js", + "Promptmodules/modular-prompt-module.js", + "Promptmodules/original-prompt-module.js", + "Promptmodules/preset-prompt-module.js", + "Promptmodules/prompt-manager.js", + "RAGmodules/RAG_Observer.html", + "RAGmodules/RAG_Overlay.html", + "RAGmodules/rag-observer-config.js", + "ScriptoriumModules/font-font-test.html", + "ScriptoriumModules/scriptorium-agent-port.js", + "ScriptoriumModules/scriptorium-async.js", + "ScriptoriumModules/scriptorium-deck-adapter.js", + "ScriptoriumModules/scriptorium-deck-editor.js", + "ScriptoriumModules/scriptorium-deck-export.js", + "ScriptoriumModules/scriptorium-deck-renderer.js", + "ScriptoriumModules/scriptorium-document-store.js", + "ScriptoriumModules/scriptorium-dom-selection.js", + "ScriptoriumModules/scriptorium-edit-history.js", + "ScriptoriumModules/scriptorium-export-resources.js", + "ScriptoriumModules/scriptorium-export.js", + "ScriptoriumModules/scriptorium-find.js", + "ScriptoriumModules/scriptorium-flow-adapter.js", + "ScriptoriumModules/scriptorium-flow-editor.js", + "ScriptoriumModules/scriptorium-flow-export.js", + "ScriptoriumModules/scriptorium-flow-renderer.js", + "ScriptoriumModules/scriptorium-formatting.js", + "ScriptoriumModules/scriptorium-input-sync.js", + "ScriptoriumModules/scriptorium-library.js", + "ScriptoriumModules/scriptorium-lineage-store.js", + "ScriptoriumModules/scriptorium-lineage-ui.js", + "ScriptoriumModules/scriptorium-media.js", + "ScriptoriumModules/scriptorium-navigation.js", + "ScriptoriumModules/scriptorium-network-fonts.js", + "ScriptoriumModules/scriptorium-objects.js", + "ScriptoriumModules/scriptorium-pagination.js", + "ScriptoriumModules/scriptorium-pr-diff.js", + "ScriptoriumModules/scriptorium-pretext-bridge.js", + "ScriptoriumModules/scriptorium-programmable-content.js", + "ScriptoriumModules/scriptorium-render-coordinator.js", + "ScriptoriumModules/scriptorium-render-primitives.js", + "ScriptoriumModules/scriptorium-rendered-text.js", + "ScriptoriumModules/scriptorium-runtime.js", + "ScriptoriumModules/scriptorium-runtime.origin.js", + "ScriptoriumModules/scriptorium-session.js", + "ScriptoriumModules/scriptorium-settings.js", + "ScriptoriumModules/scriptorium-shell.js", + "ScriptoriumModules/scriptorium-source-editor.js", + "ScriptoriumModules/scriptorium-style-ui.js", + "ScriptoriumModules/scriptorium-svg-assets.js", + "ScriptoriumModules/scriptorium-visibility.js", + "ScriptoriumModules/scriptorium.html", + "ScriptoriumModules/scriptorium.js", + "ScriptoriumModules/vdoc-container.js", + "ScriptoriumModules/vdoc-core.js", + "ScriptoriumModules/vdoc-hybrid-compiler.js", + "ScriptoriumModules/vdoc-style-library.js", + "ScriptoriumModules/vdoc-svg-asset-library.js", + "Tavernmodules/tavern-manager.js", + "Themesmodules/themes.html", + "Themesmodules/themes.js", + "Translatormodules/translator.html", + "Translatormodules/translator.js", + "VCPDistributedServer/Plugin.js", + "VCPDistributedServer/Plugin/ChatRoomViewer/ChatRoomViewer.js", + "VCPDistributedServer/Plugin/DeepMemo/DeepMemo.js", + "VCPDistributedServer/Plugin/DeepMemo/DeepMemoService.js", + "VCPDistributedServer/Plugin/DesktopRemote/desktop-remote.js", + "VCPDistributedServer/Plugin/DistImageServer/image-server.js", + "VCPDistributedServer/Plugin/FileOperator/CodeValidator.js", + "VCPDistributedServer/Plugin/FileOperator/FileOperator.js", + "VCPDistributedServer/Plugin/LoomController/LoomControllerService.js", + "VCPDistributedServer/Plugin/MusicController/music-controller.js", + "VCPDistributedServer/Plugin/OldPowerShellExecutor/PowerShellExecutor.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PluginErrorReporter.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.html", + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "VCPDistributedServer/Plugin/PluginSourceViewer/PluginSourceViewer.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.html", + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/test_security_check.js", + "VCPDistributedServer/Plugin/PromptSponsor/prompt-sponsor.js", + "VCPDistributedServer/Plugin/ScriptoriumCollaborator/ScriptoriumCollaboratorService.js", + "VCPDistributedServer/Plugin/TopicMemo/TopicMemo.js", + "VCPDistributedServer/Plugin/TopicSponsor/topicsponsor.js", + "VCPDistributedServer/Plugin/VCPEverything/local-search-controller.js", + "VCPDistributedServer/Plugin/VCPMobileSync/config/defaults.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/db.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/hash.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/idempotency.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/logger.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/agent.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/group.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/index.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/topic.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "VCPDistributedServer/Plugin/VCPMobileSync/index.js", + "VCPDistributedServer/Plugin/VCPMobileSync/protocol.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/canonical.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/central.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/projection.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/ndjson.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/routes.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "VCPDistributedServer/Plugin/VCPMobileSync/utils/lock.js", + "VCPDistributedServer/Plugin/VCPMobileSync/utils/mime.js", + "VCPDistributedServer/Plugin/VCPSuperDice/example.js", + "VCPDistributedServer/Plugin/VCPSuperDice/superdice.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/inventory.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/locate.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/media-server.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/we-wallpaper-service.js", + "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js", + "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "VCPDistributedServer/Plugin/VChatDynamicWallpaper/plugin.js", + "VCPDistributedServer/Plugin/WindowSensor/sensor-wrapper.js", + "VCPDistributedServer/VCPDistributedServer.js", + "VCPDistributedServer/frontend-plugin-loader.js", + "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "VCPHumanToolBox/ComfyUImodules/ComfyUI_StateManager.js", + "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "VCPHumanToolBox/ComfyUImodules/PathResolver.js", + "VCPHumanToolBox/ComfyUImodules/comfyUIConfig.js", + "VCPHumanToolBox/ComfyUImodules/comfyui-ipc.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ApiConfigDialog.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_CanvasManager_JSPlumb.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager_Simplified.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor_Integration.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js", + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "VCPHumanToolBox/WorkflowEditormodules/ai/HttpAiClient.js", + "VCPHumanToolBox/WorkflowEditormodules/jsplumb.min.js", + "VCPHumanToolBox/index.html", + "VCPHumanToolBox/main.js", + "VCPHumanToolBox/preload.js", + "VCPHumanToolBox/renderer.js", + "VCPHumanToolBox/renderer_modules/config.js", + "VCPHumanToolBox/renderer_modules/tool-manager.js", + "VCPHumanToolBox/renderer_modules/ui/canvas-editor.js", + "VCPHumanToolBox/renderer_modules/ui/canvas-handler.js", + "VCPHumanToolBox/renderer_modules/ui/dynamic-image-handler.js", + "VCP文坊超级引擎-四重动画实证.html", + "VchatManager/consistency-checker.js", + "VchatManager/index.html", + "VchatManager/main.js", + "VchatManager/preload.js", + "VchatManager/script.js", + "Voicechatmodules/recognizer.html", + "Voicechatmodules/voicechat.html", + "Voicechatmodules/voicechat.js", + "WebIndexTTS2/public/app.js", + "WebIndexTTS2/public/index.html", + "WebIndexTTS2/server.js", + "main.html", + "main.js", + "migration/migrateAvatars.js", + "modules/SovitsTTS.js", + "modules/assistant/assistant-rust-adapter.js", + "modules/chat/chatContext.js", + "modules/chat/chatDomRenderer.js", + "modules/chat/chatEventContract.js", + "modules/chat/chatHistoryMutationAuthority.js", + "modules/chat/chatHistoryPersistence.js", + "modules/chat/chatOperation.js", + "modules/chat/chatPluginManifest.js", + "modules/chat/chatPresentationSkin.js", + "modules/chat/chatPresentationState.js", + "modules/chat/chatRepository.js", + "modules/chat/chatSurface.js", + "modules/chat/chatSurfaceSlots.js", + "modules/chat/chatThemePlugin.js", + "modules/chat/contentModes.js", + "modules/chat/contentRuntime.js", + "modules/chat/contentTransforms.js", + "modules/chat/mainChatStateAuthority.js", + "modules/chat/memoryChatRepository.js", + "modules/chat/streamConsumerRegistry.js", + "modules/chat/streamCoordinator.js", + "modules/chat/streamSession.js", + "modules/chat/streamTransientHistory.js", + "modules/chat/surfaceConversation.js", + "modules/chat/vcpStreamBridge.js", + "modules/chatManager.js", + "modules/contextSanitizer.js", + "modules/emoticonManager.js", + "modules/event-listeners.js", + "modules/fileManager.js", + "modules/filterManager.js", + "modules/global-settings-manager.js", + "modules/image-viewer.html", + "modules/image-viewer.js", + "modules/inputEnhancer.js", + "modules/interruptHandler.js", + "modules/ipc/agentHandlers.js", + "modules/ipc/assistantHandlers.js", + "modules/ipc/canvasHandlers.js", + "modules/ipc/chatHandlers.js", + "modules/ipc/deepWikiHandlers.js", + "modules/ipc/desktopHandlers.js", + "modules/ipc/desktopMetrics.js", + "modules/ipc/desktopRemoteHandlers.js", + "modules/ipc/diceHandlers.js", + "modules/ipc/docxHandlers.js", + "modules/ipc/emoticonHandlers.js", + "modules/ipc/fileDialogHandlers.js", + "modules/ipc/forumHandlers.js", + "modules/ipc/groupChatHandlers.js", + "modules/ipc/ipcContracts.js", + "modules/ipc/libreHardwareMonitorBridge.js", + "modules/ipc/memoHandlers.js", + "modules/ipc/musicHandlers.js", + "modules/ipc/notesHandlers.js", + "modules/ipc/promptHandlers.js", + "modules/ipc/ragHandlers.js", + "modules/ipc/regexHandlers.js", + "modules/ipc/settingsHandlers.js", + "modules/ipc/sovitsHandlers.js", + "modules/ipc/tavernHandlers.js", + "modules/ipc/themeHandlers.js", + "modules/ipc/translatorHandlers.js", + "modules/ipc/voiceHandlers.js", + "modules/ipc/windowHandlers.js", + "modules/itemListManager.js", + "modules/loom/VCPLoomManager.js", + "modules/loom/webcore/adapter-contract.js", + "modules/loom/webcore/chrome-adapter.js", + "modules/loom/webcore/electron-adapter.js", + "modules/loom/webcore/index.js", + "modules/loom/webcore/web-agent-page-core.js", + "modules/loom/webcore/web-agent-page-runtime-core.js", + "modules/loom/webcore/web-agent-protocol.js", + "modules/loom/webcore/web-agent-runtime-core.js", + "modules/lyricFetcher.js", + "modules/mainChatCommands.js", + "modules/messageRenderer.js", + "modules/modelUsageTracker.js", + "modules/musicScannerWorker.js", + "modules/notificationRenderer.js", + "modules/renderer/animation.js", + "modules/renderer/colorUtils.js", + "modules/renderer/contentPipeline.js", + "modules/renderer/contentProcessor.js", + "modules/renderer/desktopPushConsumer.js", + "modules/renderer/domBuilder.js", + "modules/renderer/domListenerOwner.js", + "modules/renderer/emoticonUrlFixer.js", + "modules/renderer/enhancedColorUtils.js", + "modules/renderer/forwardMessageOwner.js", + "modules/renderer/imageHandler.js", + "modules/renderer/mainChatAttachmentOwner.js", + "modules/renderer/mainChatAuxiliaryEventOwner.js", + "modules/renderer/mainChatComposition.js", + "modules/renderer/mainChatDomBindings.js", + "modules/renderer/mainChatEventBridge.js", + "modules/renderer/mainChatFlowlockOwner.js", + "modules/renderer/mainChatSendOwner.js", + "modules/renderer/mainChatSettingsOwner.js", + "modules/renderer/mainChatSettingsPresentationOwner.js", + "modules/renderer/mainChatStreamConsumer.js", + "modules/renderer/mainChatSurfaceAdapter.js", + "modules/renderer/mainChatThemeOwner.js", + "modules/renderer/messageContextMenu.js", + "modules/renderer/middleClickHandler.js", + "modules/renderer/nonStreamingEventConsumer.js", + "modules/renderer/ownedPreloadSubscription.js", + "modules/renderer/pretext-bridge.js", + "modules/renderer/pretext.bundle.js", + "modules/renderer/pretext.esm.js", + "modules/renderer/renderDependencies.js", + "modules/renderer/renderSessionAuthority.js", + "modules/renderer/streamManager.js", + "modules/renderer/streamProjectionRuntime.js", + "modules/renderer/surfaceTaskOwner.js", + "modules/renderer/toolRequestScanner.js", + "modules/renderer/topicSelectionReadiness.js", + "modules/renderer/ttsSurfaceOwner.js", + "modules/renderer/visibilityOptimizer.js", + "modules/renderer/windowStreamRuntime.js", + "modules/searchManager.js", + "modules/services/chatDataService/client.js", + "modules/services/chatDataService/index.js", + "modules/services/chatDataService/lifecycle.js", + "modules/services/deepWikiService.js", + "modules/services/embeddedAppSessionManager.js", + "modules/services/historyWatcherLeaseManager.js", + "modules/services/networkNotesCacheStore.js", + "modules/services/preloadPaths.js", + "modules/services/scriptoriumAgentControlService.js", + "modules/services/scriptoriumFontCacheService.js", + "modules/services/scriptoriumImportService.js", + "modules/services/scriptoriumPptxImportService.js", + "modules/services/senderTaskRegistry.js", + "modules/services/windowAppIds.js", + "modules/services/windowService.js", + "modules/services/windowStateService.js", + "modules/settingsManager.js", + "modules/shared/embeddedAppAllowlist.js", + "modules/speechRecognizer.js", + "modules/tavernRulesEngine.js", + "modules/text-viewer.html", + "modules/text-viewer.js", + "modules/topTabManager.js", + "modules/topicListManager.js", + "modules/topicSummarizer.js", + "modules/trayManager.js", + "modules/ui-helpers.js", + "modules/ui-system/appearance-engine.js", + "modules/ui-system/appearance-studio.js", + "modules/ui-system/ask-nova-modal.js", + "modules/ui-system/avatar-picker.js", + "modules/ui-system/component-manifest.js", + "modules/ui-system/component-showcase.js", + "modules/ui-system/contribution-registry.js", + "modules/ui-system/interactive-chat-app.js", + "modules/ui-system/lifecycle-inspector.js", + "modules/ui-system/lifecycle-scope.js", + "modules/ui-system/lucide-adapter.js", + "modules/ui-system/next-shell/account-menu-controller.js", + "modules/ui-system/next-shell/app-tab-host.js", + "modules/ui-system/next-shell/assistant-search-controller.js", + "modules/ui-system/next-shell/creation-controller.js", + "modules/ui-system/next-shell/embedded-app-controller.js", + "modules/ui-system/next-shell/escape-dispatcher.js", + "modules/ui-system/next-shell/launchpad-controller.js", + "modules/ui-system/next-shell/next-shell-controller.js", + "modules/ui-system/next-shell/notification-menu-controller.js", + "modules/ui-system/next-shell/overlay-coordinator.js", + "modules/ui-system/next-ui-apps.js", + "modules/ui-system/performance-recorder.js", + "modules/ui-system/settings-bridge.js", + "modules/ui-system/settlement.js", + "modules/ui-system/sidebar-resizer.js", + "modules/ui-system/standalone-chat-app.js", + "modules/ui-system/startup-theme-gate.js", + "modules/ui-system/state-channel.js", + "modules/ui-system/surface-controller.js", + "modules/ui-system/task-handle.js", + "modules/ui-system/ui-surface-policy.js", + "modules/ui-system/vcp-main-ui-runtime.js", + "modules/ui-system/vcp-ui.js", + "modules/ui-system/webawesome-adapter.js", + "modules/ui-system/webawesome-comparison.js", + "modules/ui-system/webawesome-runtime-manifest.js", + "modules/uiManager.js", + "modules/utils/agentConfigManager.js", + "modules/utils/appSettingsManager.js", + "modules/vcpClient.js", + "modules/weatherService.js", + "modules/webdavManager.js", + "preload.js", + "preloads/chat.js", + "preloads/desktop.js", + "preloads/docx.js", + "preloads/loom-page.js", + "preloads/loom.js", + "preloads/shared/apiFactory.js", + "preloads/shared/catalog.js", + "preloads/shared/roles.js", + "preloads/utility.js", + "renderer.js", + "rust_assistant_engine/ui/assistant-bar.html", + "rust_assistant_engine/ui/assistant-bar.js", + "rust_assistant_engine/ui/assistant.html", + "rust_assistant_engine/ui/assistant.js", + "rust_chat_data_service/build-runtime.js", + "splash.html", + "test.html" + ], + "testFiles": [ + "tests/chat-context.test.mjs", + "tests/chat-dom-renderer.test.mjs", + "tests/chat-event-contract.test.mjs", + "tests/chat-history-mutation-authority.test.mjs", + "tests/chat-history-persistence.test.mjs", + "tests/chat-manager-selection-race.test.js", + "tests/chat-operation.test.mjs", + "tests/chat-plugin-manifest.test.mjs", + "tests/chat-presentation-skin.test.mjs", + "tests/chat-presentation-state.test.mjs", + "tests/chat-repository.test.mjs", + "tests/chat-surface-slots.test.mjs", + "tests/chat-surface.test.mjs", + "tests/chat-theme-plugin.test.mjs", + "tests/enhanced-color-utils-lifecycle.test.mjs", + "tests/lifecycle-inspector.test.js", + "tests/lifecycle-scope.test.js", + "tests/main-chat-attachment-owner.test.mjs", + "tests/main-chat-auxiliary-event-owner.test.mjs", + "tests/main-chat-dom-bindings.test.mjs", + "tests/main-chat-event-bridge.test.mjs", + "tests/main-chat-flowlock-owner.test.mjs", + "tests/main-chat-send-owner.test.mjs", + "tests/main-chat-sequence-model.test.js", + "tests/main-chat-settings-owner.test.mjs", + "tests/main-chat-state-authority.test.mjs", + "tests/main-chat-surface-adapter.test.mjs", + "tests/main-chat-theme-owner.test.mjs", + "tests/memory-chat-repository.test.mjs", + "tests/mobile-sync-streaming.test.js", + "tests/non-streaming-event-consumer.test.mjs", + "tests/notification-renderer-lifecycle.test.mjs", + "tests/stream-consumer-registry.test.mjs", + "tests/stream-coordinator.test.mjs", + "tests/stream-manager-terminal-cleanup.test.js", + "tests/stream-session.test.mjs", + "tests/stream-transient-history.test.mjs", + "tests/topic-list-mode-lifecycle.test.js", + "tests/ui-manager-lifecycle.test.mjs", + "tests/vcp-stream-bridge.test.mjs", + "tests/window-stream-runtime.test.mjs" + ], + "references": { + "chatManager": { + "production": [ + { + "file": "modules/event-listeners.js", + "line": 356, + "surface": "main-window", + "access": "explicit-provider", + "member": "attemptTopicSummarizationIfNeeded", + "snippet": "await chatManager.attemptTopicSummarizationIfNeeded();" + }, + { + "file": "modules/event-listeners.js", + "line": 431, + "surface": "main-window", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "chatManager.handleSendMessage();" + }, + { + "file": "modules/event-listeners.js", + "line": 446, + "surface": "main-window", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "chatManager.handleSendMessage();" + }, + { + "file": "modules/event-listeners.js", + "line": 1020, + "surface": "main-window", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "await chatManager.createNewTopicForItem(currentSelectedItem.id, currentSelectedItem.type);" + }, + { + "file": "modules/event-listeners.js", + "line": 1100, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "if (chatManager && chatManager.selectTopic) {" + }, + { + "file": "modules/event-listeners.js", + "line": 1101, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "await chatManager.selectTopic(result.topicId);" + }, + { + "file": "modules/event-listeners.js", + "line": 1669, + "surface": "main-window", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "if (chatManager && chatManager.createNewTopicForItem) {" + }, + { + "file": "modules/event-listeners.js", + "line": 1670, + "surface": "main-window", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "chatManager.createNewTopicForItem(currentSelectedItem.id, currentSelectedItem.type);" + }, + { + "file": "modules/renderer/forwardMessageOwner.js", + "line": 37, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "handleForwardMessage", + "snippet": "await chatManager.handleForwardMessage(target, content, original.attachments || []); uiHelperFunctions?.showToastNotification?.(`消息已转发给 ${target.name}`, 'success'); uiHelperFunctions?.closeModal?.('forwardMessageModal'); message = null; tar" + }, + { + "file": "modules/searchManager.js", + "line": 570, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "await this.chatManager.selectItem(itemId, itemType, itemName, itemAvatar, itemConfig);" + }, + { + "file": "modules/searchManager.js", + "line": 572, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "await this.chatManager.selectTopic(topicId);" + }, + { + "file": "modules/ui-system/interactive-chat-app.js", + "line": 35, + "surface": "internal-app", + "access": "explicit-provider", + "member": "sendMessage", + "snippet": "return await chatManager.sendMessage({" + }, + { + "file": "renderer.js", + "line": 24, + "surface": "main-window", + "access": "explicit-provider", + "member": "js", + "snippet": "import { chatManager } from './modules/chatManager.js';" + }, + { + "file": "renderer.js", + "line": 437, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "return chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig);" + }, + { + "file": "renderer.js", + "line": 480, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "return chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig);" + }, + { + "file": "renderer.js", + "line": 496, + "surface": "main-window", + "access": "explicit-provider", + "member": "displayTopicTimestampBubble", + "snippet": "return chatManager.displayTopicTimestampBubble(itemId, itemType, topicId);" + }, + { + "file": "renderer.js", + "line": 539, + "surface": "main-window", + "access": "explicit-provider", + "member": "processFilesData", + "snippet": "processFilesData: (...args) => chatManager.processFilesData(...args)," + }, + { + "file": "renderer.js", + "line": 540, + "surface": "main-window", + "access": "explicit-provider", + "member": "addAttachmentsToMessage", + "snippet": "addAttachmentsToMessage: (...args) => chatManager.addAttachmentsToMessage(...args)," + }, + { + "file": "renderer.js", + "line": 541, + "surface": "main-window", + "access": "explicit-provider", + "member": "removeAttachmentFromMessage", + "snippet": "removeAttachmentFromMessage: (...args) => chatManager.removeAttachmentFromMessage(...args)," + }, + { + "file": "renderer.js", + "line": 542, + "surface": "main-window", + "access": "explicit-provider", + "member": "syncNextUiEmptyStateWithMessages", + "snippet": "syncNextUiEmptyStateWithMessages: (...args) => chatManager.syncNextUiEmptyStateWithMessages(...args)," + }, + { + "file": "renderer.js", + "line": 543, + "surface": "main-window", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "handleSendMessage: (...args) => chatManager.handleSendMessage(...args)," + }, + { + "file": "renderer.js", + "line": 725, + "surface": "main-window", + "access": "explicit-provider", + "member": "handleTopicDeletion", + "snippet": "return chatManager.handleTopicDeletion(remainingTopics, deletionContext);" + }, + { + "file": "renderer.js", + "line": 732, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "return chatManager.selectTopic(topicId);" + }, + { + "file": "renderer.js", + "line": 745, + "surface": "main-window", + "access": "explicit-provider", + "member": "init", + "snippet": "chatManager.init({" + }, + { + "file": "renderer.js", + "line": 849, + "surface": "main-window", + "access": "explicit-provider", + "member": "displayNoItemSelected", + "snippet": "chatManager.displayNoItemSelected();" + }, + { + "file": "renderer.js", + "line": 873, + "surface": "main-window", + "access": "explicit-provider", + "member": "restoreLastOpenState", + "snippet": "await chatManager.restoreLastOpenState(getGlobalSettings());" + }, + { + "file": "renderer.js", + "line": 1035, + "surface": "main-window", + "access": "explicit-provider", + "member": "displayNoItemSelected", + "snippet": "chatManager.displayNoItemSelected();" + }, + { + "file": "renderer.js", + "line": 1071, + "surface": "main-window", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "Promise.resolve(chatManager.selectTopic(pending.topicId)).catch((error) => {" + }, + { + "file": "renderer.js", + "line": 1121, + "surface": "main-window", + "access": "explicit-provider", + "member": "syncHistoryFromFile", + "snippet": "if (chatManager && typeof chatManager.syncHistoryFromFile === 'function') {" + }, + { + "file": "renderer.js", + "line": 1122, + "surface": "main-window", + "access": "explicit-provider", + "member": "syncHistoryFromFile", + "snippet": "await chatManager.syncHistoryFromFile(agentId, selectedItem.type, topicId);" + } + ], + "tests": [ + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 26, + "surface": "test", + "access": "explicit-provider", + "member": "js", + "snippet": "window.eval(`${fs.readFileSync('modules/chatManager.js', 'utf8').replace(/\\bexport\\s+(?=const\\s+chatManager\\b)/, '')}\\nwindow.__testChatManager = chatManager;`);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 168, + "surface": "test", + "access": "compatibility-global", + "member": "init", + "snippet": "window.chatManager.init({" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 277, + "surface": "test", + "access": "explicit-provider", + "member": "isReady", + "snippet": "assert.equal(fixture.chatManager.isReady(), false);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 282, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 284, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 302, + "surface": "test", + "access": "explicit-provider", + "member": "restoreLastOpenState", + "snippet": "const restoring = fixture.chatManager.restoreLastOpenState({" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 322, + "surface": "test", + "access": "explicit-provider", + "member": "restoreLastOpenState", + "snippet": "const restoring = fixture.chatManager.restoreLastOpenState({" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 342, + "surface": "test", + "access": "explicit-provider", + "member": "restoreLastOpenState", + "snippet": "const restoring = fixture.chatManager.restoreLastOpenState({" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 349, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selecting = fixture.chatManager.selectItem(" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 371, + "surface": "test", + "access": "explicit-provider", + "member": "restoreLastOpenState", + "snippet": "assert.equal(await fixture.chatManager.restoreLastOpenState({" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 382, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 388, + "surface": "test", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "const first = fixture.chatManager.selectTopic('topic-b-2');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 390, + "surface": "test", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "const second = fixture.chatManager.selectTopic('topic-b');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 406, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selection = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 421, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 428, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "const sending = fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 431, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 454, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 461, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "await fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 473, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 486, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "const sending = fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 505, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 512, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "await fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 523, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 531, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "const firstSend = fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 535, + "surface": "test", + "access": "explicit-provider", + "member": "handleSendMessage", + "snippet": "const secondSend = fixture.chatManager.handleSendMessage();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 550, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 557, + "surface": "test", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "const first = fixture.chatManager.selectTopic('topic-b-2');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 559, + "surface": "test", + "access": "explicit-provider", + "member": "selectTopic", + "snippet": "const second = fixture.chatManager.selectTopic('topic-b');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 574, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 579, + "surface": "test", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "const creating = fixture.chatManager.createNewTopicForItem('agent-a', 'agent');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 581, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 599, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 604, + "surface": "test", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "const first = fixture.chatManager.createNewTopicForItem('agent-a', 'agent');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 605, + "surface": "test", + "access": "explicit-provider", + "member": "createNewTopicForItem", + "snippet": "const second = fixture.chatManager.createNewTopicForItem('agent-a', 'agent');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 621, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 626, + "surface": "test", + "access": "explicit-provider", + "member": "handleTopicDeletion", + "snippet": "const applied = await fixture.chatManager.handleTopicDeletion(" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 646, + "surface": "test", + "access": "explicit-provider", + "member": "dispose", + "snippet": "await fixture.chatManager.dispose();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 647, + "surface": "test", + "access": "explicit-provider", + "member": "dispose", + "snippet": "await fixture.chatManager.dispose();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 653, + "surface": "test", + "access": "explicit-provider", + "member": "isReady", + "snippet": "assert.equal(fixture.chatManager.isReady(), false);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 659, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 668, + "surface": "test", + "access": "explicit-provider", + "member": "syncHistoryFromFile", + "snippet": "const sync = fixture.chatManager.syncHistoryFromFile('agent-b', 'agent', 'topic-b');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 671, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selectedA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 688, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 697, + "surface": "test", + "access": "explicit-provider", + "member": "syncHistoryFromFile", + "snippet": "const sync = fixture.chatManager.syncHistoryFromFile('agent-b', 'agent', 'topic-b');" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 700, + "surface": "test", + "access": "explicit-provider", + "member": "dispose", + "snippet": "const disposal = fixture.chatManager.dispose();" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 720, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 739, + "surface": "test", + "access": "explicit-provider", + "member": "selectItem", + "snippet": "const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']);" + }, + { + "file": "tests/chat-manager-selection-race.test.js", + "line": 748, + "surface": "test", + "access": "explicit-provider", + "member": "syncHistoryFromFile", + "snippet": "const sync = fixture.chatManager.syncHistoryFromFile('agent-a', 'agent', 'topic-a');" + } + ] + }, + "messageRenderer": { + "production": [ + { + "file": "Groupmodules/grouprenderer.js", + "line": 342, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentSelectedItem", + "snippet": "messageRenderer.setCurrentSelectedItem(currentSelectedItemRef.get());" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 343, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(null);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 344, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatar", + "snippet": "messageRenderer.setCurrentItemAvatar(groupAvatarUrl); // Use group avatar - CORRECTED FUNCTION NAME" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 345, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(groupConfig?.avatarCalculatedColor || null); // CORRECTED FUNCTION NAME" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 369, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(topicToLoadId);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 373, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `加载话题列表失败: ${topics.error}`, timestamp: Date.now() });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 379, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(defaultTopicResult.topicId);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 382, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `创建默认话题失败: ${defaultTopicResult.error}`, timestamp: Date.now() });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 388, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `选择群组时出错: ${e.message}`, timestamp: Date.now() });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 833, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatar", + "snippet": "messageRenderer.setCurrentItemAvatar(result.agentGroup.avatarUrl);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 834, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(result.agentGroup.avatarCalculatedColor); // Update avatar color" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 909, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "if (messageRenderer) messageRenderer.clearChat();" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 916, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatar", + "snippet": "messageRenderer.setCurrentItemAvatar(null);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 917, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(null);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1030, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(topicId);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1075, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(null);" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1076, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "messageRenderer.clearChat();" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1251, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage(userMessageForUI); // Render user's own message in UI" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1304, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "messageRenderer.clearChat();" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1310, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: errorMsg, timestamp: Date.now() });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1314, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', name: '系统', content: '加载聊天记录中...', timestamp: Date.now(), isThinking: true, id: 'loading_history' });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1318, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1323, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `加载群聊记录失败: ${history.error}`, timestamp: Date.now() });" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1326, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "history.forEach(msg => messageRenderer.renderMessage(msg, true)); // Render silently" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1329, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 1330, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `加载群聊记录时出错: ${error.message}`, timestamp: Date.now() });" + }, + { + "file": "Voicechatmodules/voicechat.html", + "line": 9, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "css", + "snippet": "" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 7, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createMessageRenderer } from '../modules/messageRenderer.js';" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 98, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "disposeRootResources", + "snippet": "await messageRenderer.disposeRootResources(chatMessagesDiv);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 99, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "disposeRendererResources", + "snippet": "messageRenderer.disposeRendererResources();" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 298, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "initializeMessageRenderer", + "snippet": "messageRenderer.initializeMessageRenderer({" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 365, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "await messageRenderer.renderMessage(userMessage);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 384, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "await messageRenderer.renderMessage(assistantMessagePlaceholder);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 446, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "extractSpeakableTextFromContentElement", + "snippet": "textToSpeak = messageRenderer.extractSpeakableTextFromContentElement(contentElement);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 484, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "extractSpeakableTextFromContentElement", + "snippet": "? messageRenderer.extractSpeakableTextFromContentElement(contentElement)" + }, + { + "file": "modules/chatManager.js", + "line": 638, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentSelectedItem", + "snippet": "messageRenderer.setCurrentSelectedItem(currentSelectedItem);" + }, + { + "file": "modules/chatManager.js", + "line": 639, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(null);" + }, + { + "file": "modules/chatManager.js", + "line": 640, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatar", + "snippet": "messageRenderer.setCurrentItemAvatar(itemAvatarUrl);" + }, + { + "file": "modules/chatManager.js", + "line": 641, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(itemFullConfig?.avatarCalculatedColor || null);" + }, + { + "file": "modules/chatManager.js", + "line": 697, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(topicToLoadId);" + }, + { + "file": "modules/chatManager.js", + "line": 702, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载话题列表失败: ${topics.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 711, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载助手配置失败: ${agentConfig.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 718, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(defaultTopicResult.topicId);" + }, + { + "file": "modules/chatManager.js", + "line": 721, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `创建默认话题失败: ${defaultTopicResult.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 732, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(defaultTopicResult.topicId);" + }, + { + "file": "modules/chatManager.js", + "line": 735, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `创建默认群聊话题失败: ${defaultTopicResult.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 743, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `选择${itemType === 'group' ? '群组' : '助手'}时出错: ${e.message}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 832, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(topicId);" + }, + { + "file": "modules/chatManager.js", + "line": 865, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({" + }, + { + "file": "modules/chatManager.js", + "line": 911, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(null);" + }, + { + "file": "modules/chatManager.js", + "line": 912, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "messageRenderer.clearChat();" + }, + { + "file": "modules/chatManager.js", + "line": 913, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: '所有话题均已删除。请创建一个新话题。', timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 936, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "if (messageRenderer) messageRenderer.clearChat();" + }, + { + "file": "modules/chatManager.js", + "line": 947, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(topicId);" + }, + { + "file": "modules/chatManager.js", + "line": 953, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: errorMsg, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 959, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: '请选择或创建一个话题以开始聊天。', timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 972, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "await messageRenderer.renderMessage({ role: 'system', name: '系统', content: '加载聊天记录中...', timestamp: Date.now(), isThinking: true, id: 'loading_history' });" + }, + { + "file": "modules/chatManager.js", + "line": 975, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "if (messageRenderer) messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "modules/chatManager.js", + "line": 982, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "if (messageRenderer) messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "modules/chatManager.js", + "line": 995, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "if (messageRenderer) messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "modules/chatManager.js", + "line": 999, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "if (messageRenderer) messageRenderer.removeMessageById('loading_history');" + }, + { + "file": "modules/chatManager.js", + "line": 1005, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载话题 \"${topicId}\" 的聊天记录失败: ${historyResult.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 1035, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载话题 \"${topicId}\" 的聊天记录时返回了无效数据。`, timestamp: Date.now() });" + }, + { + "file": "modules/chatManager.js", + "line": 1092, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageUI", + "snippet": "if (messageRenderer && typeof messageRenderer.updateMessageUI === 'function') {" + }, + { + "file": "modules/chatManager.js", + "line": 1093, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageUI", + "snippet": "await messageRenderer.updateMessageUI(messageId, updatedMessage);" + }, + { + "file": "modules/chatManager.js", + "line": 1193, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageUI", + "snippet": "if (messageRenderer && typeof messageRenderer.updateMessageUI === 'function') {" + }, + { + "file": "modules/chatManager.js", + "line": 1194, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageUI", + "snippet": "await messageRenderer.updateMessageUI(messageId, message);" + }, + { + "file": "modules/chatManager.js", + "line": 1297, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "summarizeTopicFromMessages", + "snippet": "if (messageRenderer && typeof messageRenderer.summarizeTopicFromMessages === 'function') {" + }, + { + "file": "modules/chatManager.js", + "line": 1298, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "summarizeTopicFromMessages", + "snippet": "const summarizedTitle = await messageRenderer.summarizeTopicFromMessages(currentChatHistory.filter(m => !m.isThinking), currentAgentName);" + }, + { + "file": "modules/chatManager.js", + "line": 1861, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "startStreamingMessage", + "snippet": "start: message => (renderTarget.startStreaming || messageRenderer.startStreamingMessage).call(renderTarget, message, thinkingMessageItem)," + }, + { + "file": "modules/chatManager.js", + "line": 2046, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "messageRenderer.setCurrentTopicId(result.topicId);" + }, + { + "file": "modules/chatManager.js", + "line": 2047, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "clearChat", + "snippet": "messageRenderer.clearChat();" + }, + { + "file": "modules/chatManager.js", + "line": 2160, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentTopicId", + "snippet": "if (messageRenderer) messageRenderer.setCurrentTopicId(newTopicId);" + }, + { + "file": "modules/chatManager.js", + "line": 2322, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "removeMessageById", + "snippet": "messageRenderer.removeMessageById(oldMsg.id, false); // Update UI" + }, + { + "file": "modules/chatManager.js", + "line": 2330, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageContent", + "snippet": "if (typeof messageRenderer.updateMessageContent === 'function') {" + }, + { + "file": "modules/chatManager.js", + "line": 2331, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "updateMessageContent", + "snippet": "messageRenderer.updateMessageContent(oldMsg.id, newMsgData.content); // Update UI" + }, + { + "file": "modules/chatManager.js", + "line": 2346, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage(newMsg, true); // Update UI (true = don't modify history ref inside)" + }, + { + "file": "modules/event-listeners.js", + "line": 328, + "surface": "main-window", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "messageRenderer.renderMessage({ role: 'system', content: `VCP错误: ${response.error}`, timestamp: Date.now() });" + }, + { + "file": "modules/renderer/mainChatComposition.js", + "line": 56, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "disposeRootResources", + "snippet": "await messageRenderer.disposeRootResources(root);" + }, + { + "file": "modules/renderer/mainChatComposition.js", + "line": 57, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "disposeRendererResources", + "snippet": "messageRenderer.disposeRendererResources();" + }, + { + "file": "modules/renderer/mainChatComposition.js", + "line": 75, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setContextMenuDependencies", + "snippet": "messageRenderer.setContextMenuDependencies({" + }, + { + "file": "modules/renderer/mainChatSurfaceAdapter.js", + "line": 67, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "services.messageRenderer.renderMessage({ role: 'system', content: `流处理错误 (ID: ${event.messageId}): ${error}`, timestamp: Date.now(), id: `err_${event.messageId}` });" + }, + { + "file": "modules/renderer/nonStreamingEventConsumer.js", + "line": 11, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "renderFullMessageProjection", + "snippet": "if (relevant) await messageRenderer.renderFullMessageProjection?.(" + }, + { + "file": "modules/settingsManager.js", + "line": 412, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(avgColor);" + }, + { + "file": "modules/settingsManager.js", + "line": 474, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatar", + "snippet": "messageRenderer.setCurrentItemAvatar(updatedAgentConfig.avatarUrl);" + }, + { + "file": "modules/settingsManager.js", + "line": 475, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "setCurrentItemAvatarColor", + "snippet": "messageRenderer.setCurrentItemAvatarColor(updatedAgentConfig.avatarCalculatedColor || null);" + }, + { + "file": "modules/text-viewer.html", + "line": 9, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "css", + "snippet": "" + }, + { + "file": "renderer.js", + "line": 5, + "surface": "main-window", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createMessageRenderer } from './modules/messageRenderer.js';" + }, + { + "file": "rust_assistant_engine/ui/assistant.html", + "line": 17, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "css", + "snippet": "" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 7, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createMessageRenderer } from '../../modules/messageRenderer.js';" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 64, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "disposeRootResources", + "snippet": "await messageRenderer.disposeRootResources(chatMessagesDiv);" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 65, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "disposeRendererResources", + "snippet": "messageRenderer.disposeRendererResources();" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 222, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "initializeMessageRenderer", + "snippet": "messageRenderer.initializeMessageRenderer({" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 351, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "await messageRenderer.renderMessage(userMessage, false);" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 374, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "renderMessage", + "snippet": "await messageRenderer.renderMessage(assistantMessagePlaceholder, false);" + } + ], + "tests": [] + }, + "streamManager": { + "production": [ + { + "file": "Voicechatmodules/voicechat.js", + "line": 8, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createStreamProjection } from '../modules/renderer/streamManager.js';" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 97, + "surface": "voice-chat", + "access": "explicit-provider", + "member": "dispose", + "snippet": "await streamManager.dispose();" + }, + { + "file": "modules/messageRenderer.js", + "line": 2588, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "discardStreamingMessage", + "snippet": "discardStreamingMessage: streamManager.discardStreamingMessage," + }, + { + "file": "modules/messageRenderer.js", + "line": 2601, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "attachStreamProjection", + "snippet": "if (features.streamProjection) streamManager.attachStreamProjection({" + }, + { + "file": "modules/messageRenderer.js", + "line": 3354, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageActive", + "snippet": "&& typeof streamManager.isMessageActive === 'function'" + }, + { + "file": "modules/messageRenderer.js", + "line": 3355, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageActive", + "snippet": "&& streamManager.isMessageActive(message.id);" + }, + { + "file": "modules/messageRenderer.js", + "line": 3646, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "startStreamingMessage", + "snippet": "return streamManager.startStreamingMessage(message, messageItem);" + }, + { + "file": "modules/messageRenderer.js", + "line": 3650, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "discardStreamingMessage", + "snippet": "return streamManager.discardStreamingMessage(messageId);" + }, + { + "file": "modules/messageRenderer.js", + "line": 3655, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "appendStreamChunk", + "snippet": "streamManager.appendStreamChunk(messageId, chunkData, context);" + }, + { + "file": "modules/messageRenderer.js", + "line": 3659, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "projectStreamTerminal", + "snippet": "return streamManager.projectStreamTerminal(messageId, finishReason, context, finalPayload);" + }, + { + "file": "modules/messageRenderer.js", + "line": 4213, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageInitialized", + "snippet": "if (streamManager && typeof streamManager.isMessageInitialized === 'function') {" + }, + { + "file": "modules/messageRenderer.js", + "line": 4214, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageInitialized", + "snippet": "return streamManager.isMessageInitialized(messageId);" + }, + { + "file": "modules/renderer/middleClickHandler.js", + "line": 197, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageInitialized", + "snippet": "if (callbacks.streamManager && typeof callbacks.streamManager.isMessageInitialized === 'function') {" + }, + { + "file": "modules/renderer/middleClickHandler.js", + "line": 199, + "surface": "shared-renderer", + "access": "explicit-provider", + "member": "isMessageInitialized", + "snippet": "isStreamManagerFinalized = !callbacks.streamManager.isMessageInitialized(messageId);" + }, + { + "file": "renderer.js", + "line": 7, + "surface": "main-window", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createStreamProjection } from './modules/renderer/streamManager.js';" + }, + { + "file": "renderer.js", + "line": 26, + "surface": "main-window", + "access": "explicit-provider", + "member": "getDiagnostics", + "snippet": "window.VCPLifecycleInspector?.setStreamDiagnosticsProvider?.(() => streamManager.getDiagnostics());" + }, + { + "file": "rust_assistant_engine/ui/assistant.html", + "line": 53, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "js", + "snippet": "" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 8, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "js", + "snippet": "import { createStreamProjection } from '../../modules/renderer/streamManager.js';" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 63, + "surface": "rust-assistant", + "access": "explicit-provider", + "member": "dispose", + "snippet": "await streamManager.dispose();" + } + ], + "tests": [ + { + "file": "tests/stream-manager-terminal-cleanup.test.js", + "line": 16, + "surface": "test", + "access": "explicit-provider", + "member": "js", + "snippet": "const module = await import('../modules/renderer/streamManager.js');" + } + ] + } + }, + "retiredRendererGlobals": [ + "globalSettings", + "applyChatPresentationMode", + "normalizeChatPresentationMode", + "checkMessageFilter", + "applyChatBubbleLayoutSettings" + ], + "publicFacades": { + "VCPMainChatState": { + "owner": "renderer.js", + "dynamicSmoke": "scripts/test-electron-main-chat-sequences.mjs", + "dynamicScript": "test:electron-main-chat-sequences", + "smokeAssertions": [ + "selectedConfigFrozen: snapshot.selectedItem?.config == null || Object.isFrozen(snapshot.selectedItem.config)", + "hasHistoryRef: 'historyRef' in window.VCPMainChatState", + "VCPMainChatState must remain a frozen, non-replaceable read-only plugin facade" + ], + "mutability": "frozen deep read-only snapshot facade; mutation remains in MainChatStateAuthority", + "retirement": "retain while Flowlock and AutoTTS plugin consumers use the main-chat state protocol", + "compositionMethodsCheck": false, + "production": [ + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 9, + "surface": "flowlock", + "member": "snapshot", + "snippet": "const mainChatSnapshot = () => window.VCPMainChatState?.snapshot?.() || { selectedItem: null, topicId: null };" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 118, + "surface": "flowlock", + "member": "snapshot", + "snippet": "const resolvedAgentId = agentId || window.VCPMainChatState?.snapshot?.()?.selectedItem?.id;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 638, + "surface": "flowlock", + "member": "snapshot", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 654, + "surface": "flowlock", + "member": "snapshot", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 670, + "surface": "flowlock", + "member": "snapshot", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "line": 31, + "surface": "shared-renderer", + "member": "snapshot", + "snippet": "const item = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + } + ], + "tests": [] + }, + "MainChatCommands": { + "owner": "modules/mainChatCommands.js", + "dynamicSmoke": "scripts/test-electron-ui-apps-smoke.mjs", + "dynamicScript": "test:electron-ui-apps", + "smokeAssertions": [ + "const clearResult = window.MainChatCommands.clearNotifications()", + "assert.deepEqual(parityControls.clearProtection" + ], + "mutability": "frozen command facade; composition is configured by one-shot event", + "retirement": "retain while Next shell, Classic menus or plugin commands consume it", + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 746, + "surface": "internal-app", + "member": "subscribeWindowState", + "snippet": "releaseWindowState = window.MainChatCommands?.subscribeWindowState?.(syncWindowControl) || null;" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 772, + "surface": "internal-app", + "member": "toggleTheme", + "snippet": "toggleTheme: () => window.MainChatCommands?.toggleTheme?.()," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 829, + "surface": "internal-app", + "member": "openThemes", + "snippet": "listen(document.getElementById('nextUiThemeStoreBtn'), 'click', () => window.MainChatCommands?.openThemes?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 830, + "surface": "internal-app", + "member": "toggleTheme", + "snippet": "listen(document.getElementById('nextUiThemeBtn'), 'click', () => window.MainChatCommands?.toggleTheme?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 831, + "surface": "internal-app", + "member": "openSettings", + "snippet": "listen(document.getElementById('nextUiSettingsBtn'), 'click', () => window.MainChatCommands?.openSettings?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 832, + "surface": "internal-app", + "member": "minimizeToTray", + "snippet": "listen(document.getElementById('nextUiMinimizeToTrayBtn'), 'click', () => window.MainChatCommands?.minimizeToTray?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 833, + "surface": "internal-app", + "member": "minimize", + "snippet": "listen(document.getElementById('nextUiMinimizeBtn'), 'click', () => window.MainChatCommands?.minimize?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 834, + "surface": "internal-app", + "member": "toggleMaximize", + "snippet": "listen(document.getElementById('nextUiMaximizeBtn'), 'click', () => window.MainChatCommands?.toggleMaximize?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 835, + "surface": "internal-app", + "member": "close", + "snippet": "listen(document.getElementById('nextUiCloseBtn'), 'click', () => window.MainChatCommands?.close?.());" + } + ], + "tests": [] + }, + "VCPAppearanceStudio": { + "owner": "modules/ui-system/appearance-studio.js", + "dynamicSmoke": "scripts/test-appearance-studio.mjs", + "dynamicScript": "test:appearance-studio", + "smokeAssertions": [ + "const studio = window.VCPAppearanceStudio", + "assert.equal(studio.open(", + "assert.equal(studio.isOpen(), true)" + ], + "mutability": "frozen appearance command facade; composition and disposal use internal events", + "retirement": "retain while Next shell and settings entrypoints consume it", + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 769, + "surface": "internal-app", + "member": "open", + "snippet": "openAppearance: trigger => window.VCPAppearanceStudio?.open?.({ trigger })," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 771, + "surface": "internal-app", + "member": "setThemeMode", + "snippet": "setThemeMode: mode => window.VCPAppearanceStudio?.setThemeMode?.(mode, { source: 'account-menu' })," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 773, + "surface": "internal-app", + "member": "syncAccountMenuValue", + "snippet": "syncAppearance: () => window.VCPAppearanceStudio?.syncAccountMenuValue?.()," + } + ], + "tests": [] + } + }, + "ambientFacades": { + "__desktopDebug": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 15, + "syntax": "dot-assignment", + "snippet": "window.__desktopDebug = {" + } + ], + "production": [ + { + "file": "Desktopmodules/desktop.js", + "line": 121, + "surface": "shared-renderer", + "snippet": "console.log('[VCPdesktop] Debug: window.__desktopDebug.test() to create a test widget.');" + } + ], + "tests": [] + }, + "__LocalUrlMenu": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 101, + "syntax": "dot-assignment", + "snippet": "window.__LocalUrlMenu = {" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 18, + "surface": "shared-renderer", + "snippet": "if (!window.__LocalUrlMenu) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1144, + "surface": "shared-renderer", + "snippet": "else if (window.__LocalUrlMenu) window.__LocalUrlMenu.show(e, u);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1144, + "surface": "shared-renderer", + "snippet": "else if (window.__LocalUrlMenu) window.__LocalUrlMenu.show(e, u);" + } + ], + "tests": [] + }, + "__UrlRenderer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 167, + "syntax": "dot-assignment", + "snippet": "window.__UrlRenderer = {" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1140, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.openLightbox) window.__UrlRenderer.openLightbox(u); else window.open(u, '_blank');" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1140, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.openLightbox) window.__UrlRenderer.openLightbox(u); else window.open(u, '_blank');" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1140, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.openLightbox) window.__UrlRenderer.openLightbox(u); else window.open(u, '_blank');" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1143, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.showContextMenu) window.__UrlRenderer.showContextMenu(e, u);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1143, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.showContextMenu) window.__UrlRenderer.showContextMenu(e, u);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1143, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.showContextMenu) window.__UrlRenderer.showContextMenu(e, u);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1400, + "surface": "shared-renderer", + "snippet": "img.addEventListener('click', (e) => { if (window.__UrlRenderer) { e.preventDefault(); e.stopPropagation(); window.__UrlRenderer.openLightbox(url); } });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1400, + "surface": "shared-renderer", + "snippet": "img.addEventListener('click', (e) => { if (window.__UrlRenderer) { e.preventDefault(); e.stopPropagation(); window.__UrlRenderer.openLightbox(url); } });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1401, + "surface": "shared-renderer", + "snippet": "img.addEventListener('contextmenu', (e) => { if (window.__UrlRenderer) { window.__UrlRenderer.showContextMenu(e, url); } });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1401, + "surface": "shared-renderer", + "snippet": "img.addEventListener('contextmenu', (e) => { if (window.__UrlRenderer) { window.__UrlRenderer.showContextMenu(e, url); } });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 14, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.__inited) return;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 14, + "surface": "shared-renderer", + "snippet": "if (window.__UrlRenderer && window.__UrlRenderer.__inited) return;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 107, + "surface": "shared-renderer", + "snippet": "content.addEventListener('contextmenu', (e)=>{ if (window.__UrlRenderer) window.__UrlRenderer.showContextMenu(e, state.url); });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 107, + "surface": "shared-renderer", + "snippet": "content.addEventListener('contextmenu', (e)=>{ if (window.__UrlRenderer) window.__UrlRenderer.showContextMenu(e, state.url); });" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 598, + "surface": "shared-renderer", + "snippet": "if (imgEl && window.__UrlRenderer) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 601, + "surface": "shared-renderer", + "snippet": "window.__UrlRenderer.openLightbox(url);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 604, + "surface": "shared-renderer", + "snippet": "window.__UrlRenderer.showContextMenu(e, url);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 751, + "surface": "shared-renderer", + "snippet": "if (imgs && imgs.length && window.__UrlRenderer) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 756, + "surface": "shared-renderer", + "snippet": "window.__UrlRenderer.openLightbox(u);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 759, + "surface": "shared-renderer", + "snippet": "window.__UrlRenderer.showContextMenu(e, u);" + } + ], + "tests": [] + }, + "__vcpExternalLibsLoading": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/text-viewer.js", + "line": 980, + "syntax": "dot-assignment", + "snippet": "window.__vcpExternalLibsLoading = [];" + } + ], + "production": [ + { + "file": "modules/text-viewer.js", + "line": 979, + "surface": "shared-renderer", + "snippet": "if (!window.__vcpExternalLibsLoading) {" + }, + { + "file": "modules/text-viewer.js", + "line": 982, + "surface": "shared-renderer", + "snippet": "window.__vcpExternalLibsLoading.push(loadPromise);" + }, + { + "file": "modules/text-viewer.js", + "line": 1014, + "surface": "shared-renderer", + "snippet": "if (window.__vcpExternalLibsLoading && window.__vcpExternalLibsLoading.length > 0) {" + }, + { + "file": "modules/text-viewer.js", + "line": 1014, + "surface": "shared-renderer", + "snippet": "if (window.__vcpExternalLibsLoading && window.__vcpExternalLibsLoading.length > 0) {" + }, + { + "file": "modules/text-viewer.js", + "line": 1021, + "surface": "shared-renderer", + "snippet": "if (window.__vcpExternalLibsLoading) {" + }, + { + "file": "modules/text-viewer.js", + "line": 1022, + "surface": "shared-renderer", + "snippet": "await Promise.all(window.__vcpExternalLibsLoading);" + } + ], + "tests": [] + }, + "__vcpProxyFetch": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Desktopmodules/api/vcpProxy.js", + "line": 178, + "syntax": "dot-assignment", + "snippet": "window.__vcpProxyFetch = proxyFetch;" + } + ], + "production": [ + { + "file": "Desktopmodules/builtinWidgets/newsWidget.js", + "line": 82, + "surface": "shared-renderer", + "snippet": "' var data = await window.__vcpProxyFetch(\"/admin_api/dailyhot\");'," + }, + { + "file": "Desktopmodules/builtinWidgets/weatherWidget.js", + "line": 58, + "surface": "shared-renderer", + "snippet": "' var data = await window.__vcpProxyFetch(\"/admin_api/weather\");'," + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 558, + "surface": "shared-renderer", + "snippet": "fetch: function(endpoint, opts) { return window.__vcpProxyFetch(endpoint, opts); }," + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 560, + "surface": "shared-renderer", + "snippet": "weather: function() { return window.__vcpProxyFetch('/admin_api/weather'); }," + } + ], + "tests": [] + }, + "__vcpProxyPost": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Desktopmodules/api/vcpProxy.js", + "line": 179, + "syntax": "dot-assignment", + "snippet": "window.__vcpProxyPost = vcpPost;" + } + ], + "production": [ + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 559, + "surface": "shared-renderer", + "snippet": "post: function(messages, opts) { return window.__vcpProxyPost(messages, opts); }," + } + ], + "tests": [] + }, + "__VCPRegisterScene": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 442, + "syntax": "dot-assignment", + "snippet": "window.__VCPRegisterScene = (slideIndex, factory) => {" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 439, + "syntax": "dot-assignment", + "snippet": "window.__VCPRegisterScene = (slideIndex, factory) => {" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 79, + "surface": "shared-renderer", + "snippet": "return `window.__VCPRegisterScene(${index}, (lifecycle) => {" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 524, + "surface": "shared-renderer", + "snippet": "window.__VCPRegisterScene(2, (lifecycle) => {" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 623, + "surface": "shared-renderer", + "snippet": "window.__VCPRegisterScene(3, (lifecycle) => {" + } + ], + "tests": [] + }, + "__WE_AI_MODELS__": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 165, + "syntax": "dot-assignment", + "snippet": "window.__WE_AI_MODELS__ = models;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 936, + "syntax": "dot-assignment", + "snippet": "window.__WE_AI_MODELS__ = models;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 931, + "surface": "shared-renderer", + "snippet": "if (Array.isArray(window.__WE_AI_MODELS__) && window.__WE_AI_MODELS__.length > 0) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 931, + "surface": "shared-renderer", + "snippet": "if (Array.isArray(window.__WE_AI_MODELS__) && window.__WE_AI_MODELS__.length > 0) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 932, + "surface": "shared-renderer", + "snippet": "applyOptions(window.__WE_AI_MODELS__);" + } + ], + "tests": [] + }, + "__WE_isRestoringConnections": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2074, + "syntax": "dot-assignment", + "snippet": "window.__WE_isRestoringConnections = true;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2134, + "syntax": "dot-assignment", + "snippet": "window.__WE_isRestoringConnections = false;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2190, + "syntax": "dot-assignment", + "snippet": "window.__WE_isRestoringConnections = true;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2245, + "syntax": "dot-assignment", + "snippet": "window.__WE_isRestoringConnections = false;" + } + ], + "production": [], + "tests": [] + }, + "_vcp_loaded_scripts": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/renderer/animation.js", + "line": 15, + "syntax": "dot-assignment", + "snippet": "window._vcp_loaded_scripts = new Set();" + } + ], + "production": [ + { + "file": "modules/renderer/animation.js", + "line": 14, + "surface": "shared-renderer", + "snippet": "if (!window._vcp_loaded_scripts) {" + }, + { + "file": "modules/renderer/animation.js", + "line": 156, + "surface": "shared-renderer", + "snippet": "if (window._vcp_loaded_scripts.has(src)) {" + }, + { + "file": "modules/renderer/animation.js", + "line": 160, + "surface": "shared-renderer", + "snippet": "window._vcp_loaded_scripts.add(src); // Pre-mark to prevent race conditions" + }, + { + "file": "modules/renderer/animation.js", + "line": 170, + "surface": "shared-renderer", + "snippet": "window._vcp_loaded_scripts.delete(src); // Allow retry on failure" + } + ], + "tests": [] + }, + "AiClientFactory": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 43, + "syntax": "dot-assignment", + "snippet": "window.AiClientFactory = new AiClientFactory();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 162, + "surface": "shared-renderer", + "snippet": "if (window.AiClientFactory) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 163, + "surface": "shared-renderer", + "snippet": "const client = window.AiClientFactory.getClient();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1864, + "surface": "shared-renderer", + "snippet": "if (!window.AiClientFactory) throw new Error('AI服务未初始化:缺少 AiClientFactory');" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1865, + "surface": "shared-renderer", + "snippet": "const client = window.AiClientFactory.getClient();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 933, + "surface": "shared-renderer", + "snippet": "} else if (window.AiClientFactory) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 935, + "surface": "shared-renderer", + "snippet": "window.AiClientFactory.getClient().listModels().then(models => {" + } + ], + "tests": [] + }, + "alert": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Memomodules/memo.js", + "line": 64, + "syntax": "dot-assignment", + "snippet": "window.alert = (message) => {" + } + ], + "production": [], + "tests": [] + }, + "anime": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/renderer/animation.js", + "line": 207, + "syntax": "dot-assignment", + "snippet": "window.anime = function(options) {" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 369, + "surface": "shared-renderer", + "snippet": "const scopedAnime = typeof window.anime === 'function'" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 370, + "surface": "shared-renderer", + "snippet": "? new Proxy(window.anime, {" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 384, + "surface": "shared-renderer", + "snippet": ": window.anime;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 198, + "surface": "shared-renderer", + "snippet": "if (typeof window.anime !== 'function') return window.anime;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 198, + "surface": "shared-renderer", + "snippet": "if (typeof window.anime !== 'function') return window.anime;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 233, + "surface": "shared-renderer", + "snippet": "return new Proxy(window.anime, {" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 180, + "surface": "shared-renderer", + "snippet": "if (typeof window.anime !== 'function') return window.anime;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 180, + "surface": "shared-renderer", + "snippet": "if (typeof window.anime !== 'function') return window.anime;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 187, + "surface": "shared-renderer", + "snippet": "const facade = new Proxy(window.anime, {" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 366, + "surface": "shared-renderer", + "snippet": "const scopedAnime = typeof window.anime === 'function'" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 367, + "surface": "shared-renderer", + "snippet": "? new Proxy(window.anime, {" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 381, + "surface": "shared-renderer", + "snippet": ": window.anime;" + }, + { + "file": "modules/renderer/animation.js", + "line": 204, + "surface": "shared-renderer", + "snippet": "const originalAnime = window.anime;" + }, + { + "file": "modules/renderer/animation.js", + "line": 214, + "surface": "shared-renderer", + "snippet": "Object.assign(window.anime, originalAnime);" + }, + { + "file": "modules/renderer/animation.js", + "line": 215, + "surface": "shared-renderer", + "snippet": "window.anime._vcp_patched = true;" + }, + { + "file": "modules/renderer/animation.js", + "line": 474, + "surface": "shared-renderer", + "snippet": "if (window.anime) {" + }, + { + "file": "modules/renderer/animation.js", + "line": 513, + "surface": "shared-renderer", + "snippet": "if (!window.anime) return;" + }, + { + "file": "modules/renderer/animation.js", + "line": 530, + "surface": "shared-renderer", + "snippet": "if (!window.anime) {" + }, + { + "file": "modules/text-viewer.js", + "line": 934, + "surface": "shared-renderer", + "snippet": "if (!containerElement || !window.anime) return;" + } + ], + "tests": [] + }, + "appendAgentTag": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1193, + "syntax": "dot-assignment", + "snippet": "window.appendAgentTag = (val) => {" + } + ], + "production": [], + "tests": [] + }, + "askNovaController": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 676, + "syntax": "dot-assignment", + "snippet": "window.askNovaController = createAskNovaController();" + } + ], + "production": [ + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 675, + "surface": "internal-app", + "snippet": "if (window.askNovaController) return;" + } + ], + "tests": [] + }, + "cancelDelegation": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 853, + "syntax": "dot-assignment", + "snippet": "window.cancelDelegation = async (delegationId) => {" + } + ], + "production": [], + "tests": [] + }, + "closeComfyUISettings": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1721, + "syntax": "dot-assignment", + "snippet": "window.closeComfyUISettings = closeComfyUISettings;" + } + ], + "production": [], + "tests": [] + }, + "comfyUI": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/comfyUIConfig.js", + "line": 336, + "syntax": "dot-assignment", + "snippet": "window.comfyUI = {" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 117, + "surface": "shared-renderer", + "snippet": "window.comfyUI) {" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 135, + "surface": "shared-renderer", + "snippet": "delete window.comfyUI;" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1625, + "surface": "shared-renderer", + "snippet": "if (window.comfyUI && drawerContent) {" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1626, + "surface": "shared-renderer", + "snippet": "window.comfyUI.createUI(drawerContent, {" + } + ], + "tests": [] + }, + "ComfyUI_StateManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_StateManager.js", + "line": 127, + "syntax": "dot-assignment", + "snippet": "window.ComfyUI_StateManager = ComfyUI_StateManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 115, + "surface": "shared-renderer", + "snippet": "if (window.ComfyUI_StateManager &&" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 133, + "surface": "shared-renderer", + "snippet": "delete window.ComfyUI_StateManager;" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/comfyUIConfig.js", + "line": 11, + "surface": "shared-renderer", + "snippet": "this.stateManager = window.ComfyUI_StateManager;" + } + ], + "tests": [] + }, + "ComfyUI_UIManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 635, + "syntax": "dot-assignment", + "snippet": "window.ComfyUI_UIManager = ComfyUI_UIManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 116, + "surface": "shared-renderer", + "snippet": "window.ComfyUI_UIManager &&" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 134, + "surface": "shared-renderer", + "snippet": "delete window.ComfyUI_UIManager;" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/comfyUIConfig.js", + "line": 12, + "surface": "shared-renderer", + "snippet": "this.uiManager = window.ComfyUI_UIManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2553, + "surface": "shared-renderer", + "snippet": "if (window.ComfyUI_UIManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2554, + "surface": "shared-renderer", + "snippet": "window.ComfyUI_UIManager.showToast(message, type);" + } + ], + "tests": [] + }, + "ComfyUILoader": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js", + "line": 159, + "syntax": "dot-assignment", + "snippet": "window.ComfyUILoader = new ComfyUILoader();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1621, + "surface": "shared-renderer", + "snippet": "if (window.ComfyUILoader) {" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1622, + "surface": "shared-renderer", + "snippet": "await window.ComfyUILoader.load();" + } + ], + "tests": [] + }, + "deleteAgentByIndex": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 509, + "syntax": "dot-assignment", + "snippet": "window.deleteAgentByIndex = (index) => {" + } + ], + "production": [], + "tests": [] + }, + "deleteTaskByIndex": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1001, + "syntax": "dot-assignment", + "snippet": "window.deleteTaskByIndex = (index) => {" + } + ], + "production": [], + "tests": [] + }, + "DiaryWorkbench": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Memomodules/memo-workbench.js", + "line": 352, + "syntax": "dot-assignment", + "snippet": "window.DiaryWorkbench = DiaryWorkbench;" + } + ], + "production": [ + { + "file": "Memomodules/memo-graph.js", + "line": 601, + "surface": "shared-renderer", + "snippet": "if (window.DiaryWorkbench) {" + }, + { + "file": "Memomodules/memo-graph.js", + "line": 603, + "surface": "shared-renderer", + "snippet": "await window.DiaryWorkbench.addMemos(nodesToAdd);" + }, + { + "file": "Memomodules/memo-graph.js", + "line": 605, + "surface": "shared-renderer", + "snippet": "window.DiaryWorkbench.overlay.style.display = 'flex';" + }, + { + "file": "Memomodules/memo-graph.js", + "line": 662, + "surface": "shared-renderer", + "snippet": "if (window.DiaryWorkbench) {" + }, + { + "file": "Memomodules/memo-graph.js", + "line": 663, + "surface": "shared-renderer", + "snippet": "window.DiaryWorkbench.open([node]);" + }, + { + "file": "Memomodules/memo.js", + "line": 107, + "surface": "shared-renderer", + "snippet": "if (window.DiaryWorkbench) {" + }, + { + "file": "Memomodules/memo.js", + "line": 108, + "surface": "shared-renderer", + "snippet": "window.DiaryWorkbench.init();" + }, + { + "file": "Memomodules/memo.js", + "line": 253, + "surface": "shared-renderer", + "snippet": "if (window.DiaryWorkbench) {" + }, + { + "file": "Memomodules/memo.js", + "line": 254, + "surface": "shared-renderer", + "snippet": "window.DiaryWorkbench.open(selected);" + } + ], + "tests": [] + }, + "diff_match_patch": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Canvasmodules/canvas.html", + "line": 166, + "syntax": "dot-assignment", + "snippet": "window.diff_match_patch = diff_match_patch;" + } + ], + "production": [], + "tests": [] + }, + "emoticonManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/emoticonManager.js", + "line": 181, + "syntax": "dot-assignment", + "snippet": "window.emoticonManager = emoticonManager;" + } + ], + "production": [ + { + "file": "Forummodules/forum.js", + "line": 75, + "surface": "shared-renderer", + "snippet": "if (window.emoticonManager && emoticonPanel) {" + }, + { + "file": "Forummodules/forum.js", + "line": 76, + "surface": "shared-renderer", + "snippet": "await window.emoticonManager.initialize({ emoticonPanel });" + }, + { + "file": "Forummodules/forum.js", + "line": 1471, + "surface": "shared-renderer", + "snippet": "window.emoticonManager.togglePanel(replyEmoticonBtn, textInput);" + }, + { + "file": "Forummodules/forum.js", + "line": 1645, + "surface": "shared-renderer", + "snippet": "window.emoticonManager.togglePanel(createPostEmoticonBtn, postContentInput);" + }, + { + "file": "renderer.js", + "line": 397, + "surface": "main-window", + "snippet": "if (window.emoticonManager) {" + }, + { + "file": "renderer.js", + "line": 398, + "surface": "main-window", + "snippet": "window.emoticonManager.initialize({" + }, + { + "file": "renderer.js", + "line": 966, + "surface": "main-window", + "snippet": "if (attachFileBtn && emoticonTriggerBtn && window.emoticonManager) {" + }, + { + "file": "renderer.js", + "line": 974, + "surface": "main-window", + "snippet": "window.emoticonManager.togglePanel(emoticonTriggerBtn);" + } + ], + "tests": [] + }, + "filterManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/filterManager.js", + "line": 1, + "syntax": "dot-assignment", + "snippet": "window.filterManager = (() => {" + } + ], + "production": [ + { + "file": "modules/mainChatCommands.js", + "line": 81, + "surface": "main-window", + "snippet": "return capability('filterManager', () => window.filterManager)?.toggleFilterMode?.();" + }, + { + "file": "modules/mainChatCommands.js", + "line": 85, + "surface": "main-window", + "snippet": "return capability('filterManager', () => window.filterManager)?.openFilterRulesModal?.();" + }, + { + "file": "modules/notificationRenderer.js", + "line": 98, + "surface": "shared-renderer", + "snippet": "const autoApprovalResult = (filterManagerCapability || window.filterManager)?.checkToolAutoApproval?.(logData.data);" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 783, + "surface": "internal-app", + "snippet": "filterManager: window.filterManager," + }, + { + "file": "renderer.js", + "line": 254, + "surface": "main-window", + "snippet": "filterManager: window.filterManager," + }, + { + "file": "renderer.js", + "line": 393, + "surface": "main-window", + "snippet": "filterManager: window.filterManager," + }, + { + "file": "renderer.js", + "line": 791, + "surface": "main-window", + "snippet": "filterManager: window.filterManager," + }, + { + "file": "renderer.js", + "line": 904, + "surface": "main-window", + "snippet": "if (window.filterManager) {" + }, + { + "file": "renderer.js", + "line": 905, + "surface": "main-window", + "snippet": "window.filterManager.init({" + } + ], + "tests": [] + }, + "flowlockManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Flowlockmodules/flowlock.js", + "line": 748, + "syntax": "dot-assignment", + "snippet": "window.flowlockManager = flowlockManager;" + } + ], + "production": [ + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 18, + "surface": "flowlock", + "snippet": "if (!window.flowlockManager) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 26, + "surface": "flowlock", + "snippet": "window.flowlockManager.initialize({" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 36, + "surface": "flowlock", + "snippet": "Promise.resolve(window.flowlockManager.recoverPendingRequests?.()).catch((error) => {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 295, + "surface": "flowlock", + "snippet": "await window.flowlockManager?.handleFinalizedMessage({" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 322, + "surface": "flowlock", + "snippet": "if (!window.flowlockManager) return;" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 336, + "surface": "flowlock", + "snippet": "if (window.flowlockManager.isAgentLocked(currentItem.id)) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 337, + "surface": "flowlock", + "snippet": "await window.flowlockManager.stop(currentItem.id);" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 340, + "surface": "flowlock", + "snippet": "await window.flowlockManager.start(currentItem.id, currentTopic, { startImmediately: false });" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 351, + "surface": "flowlock", + "snippet": "if (!window.flowlockManager) return;" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 355, + "surface": "flowlock", + "snippet": "if (window.flowlockManager.isAgentLocked(currentItem.id)) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 357, + "surface": "flowlock", + "snippet": "await window.flowlockManager.stop(currentItem.id);" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 367, + "surface": "flowlock", + "snippet": "await window.flowlockManager.start(currentItem.id, currentTopic, { startImmediately: true });" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 383, + "surface": "flowlock", + "snippet": "if (!window.flowlockManager) return;" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 394, + "surface": "flowlock", + "snippet": "if (window.flowlockManager.isAgentLocked(currentItem.id)) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 396, + "surface": "flowlock", + "snippet": "await window.flowlockManager.stop(currentItem.id);" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 399, + "surface": "flowlock", + "snippet": "await window.flowlockManager.start(currentItem.id, currentTopic, { startImmediately: true });" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 438, + "surface": "flowlock", + "snippet": "if (window.flowlockManager) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 439, + "surface": "flowlock", + "snippet": "window.flowlockManager.cleanup();" + }, + { + "file": "modules/chatManager.js", + "line": 657, + "surface": "shared-renderer", + "snippet": "window.flowlockManager?.syncCurrentHeaderIndicator?.();" + }, + { + "file": "modules/chatManager.js", + "line": 681, + "surface": "shared-renderer", + "snippet": "? window.flowlockManager?.getLockedTopicId?.(itemId)" + }, + { + "file": "modules/chatManager.js", + "line": 791, + "surface": "shared-renderer", + "snippet": "? window.flowlockManager?.getLockedTopicId?.(selectedItemForLock.id)" + }, + { + "file": "modules/chatManager.js", + "line": 2006, + "surface": "shared-renderer", + "snippet": "if (itemType === 'agent' && window.flowlockManager?.isAgentLocked?.(itemId)) {" + }, + { + "file": "modules/chatManager.js", + "line": 2088, + "surface": "shared-renderer", + "snippet": "if (currentSelectedItem?.type === 'agent' && window.flowlockManager?.isAgentLocked?.(currentSelectedItem.id)) {" + }, + { + "file": "modules/event-listeners.js", + "line": 459, + "surface": "main-window", + "snippet": "if (window.flowlockManager?.isAgentLocked?.(currentSelectedItem?.id)) {" + }, + { + "file": "modules/event-listeners.js", + "line": 1634, + "surface": "main-window", + "snippet": "if (window.flowlockManager?.isAgentLocked?.(refs.currentSelectedItem.get()?.id)) {" + }, + { + "file": "modules/itemListManager.js", + "line": 805, + "surface": "shared-renderer", + "snippet": "if (item.type === 'agent' && window.flowlockManager?.isAgentLocked?.(item.id)) {" + }, + { + "file": "modules/topicListManager.js", + "line": 828, + "surface": "shared-renderer", + "snippet": "? topicsToDelete.find(topic => window.flowlockManager?.isTopicLocked?.(currentSelectedItem.id, topic.id))" + }, + { + "file": "modules/topicListManager.js", + "line": 1120, + "surface": "shared-renderer", + "snippet": "if (itemType === 'agent' && window.flowlockManager?.isTopicLocked?.(itemFullConfig.id, topic.id)) {" + }, + { + "file": "renderer.js", + "line": 563, + "surface": "main-window", + "snippet": "flowlockManager: window.flowlockManager," + }, + { + "file": "renderer.js", + "line": 1146, + "surface": "main-window", + "snippet": "flowlockManager: window.flowlockManager," + } + ], + "tests": [] + }, + "getDominantAvatarColor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/renderer/colorUtils.js", + "line": 113, + "syntax": "dot-assignment", + "snippet": "window.getDominantAvatarColor = getDominantAvatarColor;" + } + ], + "production": [ + { + "file": "modules/event-listeners.js", + "line": 668, + "surface": "main-window", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/event-listeners.js", + "line": 669, + "surface": "main-window", + "snippet": "window.getDominantAvatarColor(previewUrl).then((avgColor) => {" + }, + { + "file": "modules/event-listeners.js", + "line": 712, + "surface": "main-window", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/event-listeners.js", + "line": 713, + "surface": "main-window", + "snippet": "window.getDominantAvatarColor(userAvatarPreview.src).then((avgColor) => {" + }, + { + "file": "modules/event-listeners.js", + "line": 979, + "surface": "main-window", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/event-listeners.js", + "line": 980, + "surface": "main-window", + "snippet": "window.getDominantAvatarColor(userAvatarPreview.src).then((avgColor) => {" + }, + { + "file": "modules/global-settings-manager.js", + "line": 233, + "surface": "shared-renderer", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/global-settings-manager.js", + "line": 234, + "surface": "shared-renderer", + "snippet": "window.getDominantAvatarColor(avatarSaveResult.avatarUrl).then(avgColor => {" + }, + { + "file": "modules/settingsManager.js", + "line": 874, + "surface": "shared-renderer", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/settingsManager.js", + "line": 875, + "surface": "shared-renderer", + "snippet": "window.getDominantAvatarColor(previewUrl).then((avgColor) => {" + }, + { + "file": "modules/settingsManager.js", + "line": 2250, + "surface": "shared-renderer", + "snippet": "if (window.getDominantAvatarColor) {" + }, + { + "file": "modules/settingsManager.js", + "line": 2251, + "surface": "shared-renderer", + "snippet": "window.getDominantAvatarColor(agentAvatarPreview.src).then((avgColor) => {" + } + ], + "tests": [] + }, + "GroupRenderer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Groupmodules/grouprenderer.js", + "line": 3, + "syntax": "dot-assignment", + "snippet": "window.GroupRenderer = (() => {" + } + ], + "production": [ + { + "file": "modules/settingsManager.js", + "line": 133, + "surface": "shared-renderer", + "snippet": "if (window.GroupRenderer && typeof window.GroupRenderer.displayGroupSettingsPage === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 133, + "surface": "shared-renderer", + "snippet": "if (window.GroupRenderer && typeof window.GroupRenderer.displayGroupSettingsPage === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 134, + "surface": "shared-renderer", + "snippet": "window.GroupRenderer.displayGroupSettingsPage(currentSelectedItem.id);" + }, + { + "file": "modules/ui-helpers.js", + "line": 610, + "surface": "shared-renderer", + "snippet": "else if (cropType === 'group' && window.GroupRenderer) {" + }, + { + "file": "renderer.js", + "line": 450, + "surface": "main-window", + "snippet": "if (window.GroupRenderer) {" + }, + { + "file": "renderer.js", + "line": 468, + "surface": "main-window", + "snippet": "window.GroupRenderer.init({" + }, + { + "file": "renderer.js", + "line": 506, + "surface": "main-window", + "snippet": "console.log('[Renderer POST-INIT GroupRenderer] window.GroupRenderer.init has been called.');" + }, + { + "file": "renderer.js", + "line": 756, + "surface": "main-window", + "snippet": "groupRenderer: window.GroupRenderer," + } + ], + "tests": [] + }, + "GroupSettingsMarkup": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Groupmodules/groupSettingsMarkup.js", + "line": 4, + "syntax": "dot-assignment", + "snippet": "window.GroupSettingsMarkup = (() => {" + } + ], + "production": [ + { + "file": "Groupmodules/grouprenderer.js", + "line": 107, + "surface": "shared-renderer", + "snippet": "groupSettingsContainer.innerHTML = window.GroupSettingsMarkup.renderGroupSettingsMarkup();" + } + ], + "tests": [] + }, + "handleContinueWriting": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/event-listeners.js", + "line": 380, + "syntax": "dot-assignment", + "snippet": "window.handleContinueWriting = handleContinueWriting;" + } + ], + "production": [], + "tests": [] + }, + "HttpAiClient": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/HttpAiClient.js", + "line": 104, + "syntax": "dot-assignment", + "snippet": "window.HttpAiClient = HttpAiClient;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 35, + "surface": "shared-renderer", + "snippet": "this._client = new window.HttpAiClient(baseUrl, key);" + } + ], + "tests": [] + }, + "initializeFlowlockIntegration": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 452, + "syntax": "dot-assignment", + "snippet": "window.initializeFlowlockIntegration = initializeFlowlockIntegration;" + } + ], + "production": [ + { + "file": "renderer.js", + "line": 1129, + "surface": "main-window", + "snippet": "if (window.initializeFlowlockIntegration) {" + }, + { + "file": "renderer.js", + "line": 1130, + "surface": "main-window", + "snippet": "window.initializeFlowlockIntegration({" + } + ], + "tests": [] + }, + "inputEnhancer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/inputEnhancer.js", + "line": 628, + "syntax": "dot-assignment", + "snippet": "window.inputEnhancer = {" + } + ], + "production": [ + { + "file": "renderer.js", + "line": 610, + "surface": "main-window", + "snippet": "if (window.inputEnhancer) {" + }, + { + "file": "renderer.js", + "line": 611, + "surface": "main-window", + "snippet": "window.inputEnhancer.initializeInputEnhancer({" + }, + { + "file": "renderer.js", + "line": 622, + "surface": "main-window", + "snippet": "ownedRendererSubscriptions.add({ dispose: () => window.inputEnhancer.dispose?.() });" + } + ], + "tests": [] + }, + "isThemeListenerAttached": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Canvasmodules/canvas.js", + "line": 193, + "syntax": "dot-assignment", + "snippet": "window.isThemeListenerAttached = true;" + } + ], + "production": [ + { + "file": "Canvasmodules/canvas.js", + "line": 191, + "surface": "shared-renderer", + "snippet": "if (!window.isThemeListenerAttached) {" + } + ], + "tests": [] + }, + "itemListManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/itemListManager.js", + "line": 3, + "syntax": "dot-assignment", + "snippet": "window.itemListManager = (() => {" + } + ], + "production": [ + { + "file": "modules/mainChatCommands.js", + "line": 110, + "surface": "main-window", + "snippet": "await capability('itemListManager', () => window.itemListManager)?.loadItems?.();" + }, + { + "file": "modules/mainChatCommands.js", + "line": 134, + "surface": "main-window", + "snippet": "await capability('itemListManager', () => window.itemListManager)?.loadItems?.();" + }, + { + "file": "modules/settingsManager.js", + "line": 447, + "surface": "shared-renderer", + "snippet": "await window.itemListManager.loadItems();" + }, + { + "file": "modules/settingsManager.js", + "line": 720, + "surface": "shared-renderer", + "snippet": "if (window.itemListManager && typeof window.itemListManager.resetMouseEventStates === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 720, + "surface": "shared-renderer", + "snippet": "if (window.itemListManager && typeof window.itemListManager.resetMouseEventStates === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 721, + "surface": "shared-renderer", + "snippet": "window.itemListManager.resetMouseEventStates();" + }, + { + "file": "modules/uiManager.js", + "line": 486, + "surface": "shared-renderer", + "snippet": "if ((itemListManagerCapability || window.itemListManager)?.resetMouseEventStates) {" + }, + { + "file": "modules/uiManager.js", + "line": 487, + "surface": "shared-renderer", + "snippet": "(itemListManagerCapability || window.itemListManager).resetMouseEventStates();" + }, + { + "file": "modules/uiManager.js", + "line": 509, + "surface": "shared-renderer", + "snippet": "if ((itemListManagerCapability || window.itemListManager)?.updateUnreadBadges) {" + }, + { + "file": "modules/uiManager.js", + "line": 510, + "surface": "shared-renderer", + "snippet": "(itemListManagerCapability || window.itemListManager).updateUnreadBadges(result.counts);" + }, + { + "file": "renderer.js", + "line": 424, + "surface": "main-window", + "snippet": "if (window.itemListManager) {" + }, + { + "file": "renderer.js", + "line": 425, + "surface": "main-window", + "snippet": "window.itemListManager.init({" + }, + { + "file": "renderer.js", + "line": 477, + "surface": "main-window", + "snippet": "loadItems: () => window.itemListManager ? window.itemListManager.loadItems() : console.error('[GroupRenderer] itemListManager not available')," + }, + { + "file": "renderer.js", + "line": 477, + "surface": "main-window", + "snippet": "loadItems: () => window.itemListManager ? window.itemListManager.loadItems() : console.error('[GroupRenderer] itemListManager not available')," + }, + { + "file": "renderer.js", + "line": 485, + "surface": "main-window", + "snippet": "highlightActiveItem: (itemId, itemType) => window.itemListManager ? window.itemListManager.highlightActiveItem(itemId, itemType) : console.error('[GroupRenderer] itemListManager not available')," + }, + { + "file": "renderer.js", + "line": 485, + "surface": "main-window", + "snippet": "highlightActiveItem: (itemId, itemType) => window.itemListManager ? window.itemListManager.highlightActiveItem(itemId, itemType) : console.error('[GroupRenderer] itemListManager not available')," + }, + { + "file": "renderer.js", + "line": 714, + "surface": "main-window", + "snippet": "itemListManager: window.itemListManager," + }, + { + "file": "renderer.js", + "line": 754, + "surface": "main-window", + "snippet": "itemListManager: window.itemListManager," + }, + { + "file": "renderer.js", + "line": 790, + "surface": "main-window", + "snippet": "itemListManager: window.itemListManager," + }, + { + "file": "renderer.js", + "line": 850, + "surface": "main-window", + "snippet": "await window.itemListManager.loadItems();" + }, + { + "file": "renderer.js", + "line": 872, + "surface": "main-window", + "snippet": "await window.itemListManager.loadItems(); // Load both agents and groups" + }, + { + "file": "renderer.js", + "line": 885, + "surface": "main-window", + "snippet": "itemListManager: window.itemListManager," + }, + { + "file": "renderer.js", + "line": 941, + "surface": "main-window", + "snippet": "itemListManager: window.itemListManager," + } + ], + "tests": [] + }, + "MainChatCommands": { + "classification": "supported-public-facade", + "definitions": [ + { + "file": "modules/mainChatCommands.js", + "line": 200, + "syntax": "define-property", + "snippet": "Object.defineProperty(window, 'MainChatCommands', { value: Object.freeze({" + } + ], + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 746, + "surface": "internal-app", + "snippet": "releaseWindowState = window.MainChatCommands?.subscribeWindowState?.(syncWindowControl) || null;" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 772, + "surface": "internal-app", + "snippet": "toggleTheme: () => window.MainChatCommands?.toggleTheme?.()," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 782, + "surface": "internal-app", + "snippet": "commands: () => window.MainChatCommands," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 811, + "surface": "internal-app", + "snippet": "commands: () => window.MainChatCommands," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 829, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiThemeStoreBtn'), 'click', () => window.MainChatCommands?.openThemes?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 830, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiThemeBtn'), 'click', () => window.MainChatCommands?.toggleTheme?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 831, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiSettingsBtn'), 'click', () => window.MainChatCommands?.openSettings?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 832, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiMinimizeToTrayBtn'), 'click', () => window.MainChatCommands?.minimizeToTray?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 833, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiMinimizeBtn'), 'click', () => window.MainChatCommands?.minimize?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 834, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiMaximizeBtn'), 'click', () => window.MainChatCommands?.toggleMaximize?.());" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 835, + "surface": "internal-app", + "snippet": "listen(document.getElementById('nextUiCloseBtn'), 'click', () => window.MainChatCommands?.close?.());" + } + ], + "tests": [] + }, + "ModularPromptModule": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Promptmodules/modular-prompt-module.js", + "line": 1594, + "syntax": "dot-assignment", + "snippet": "window.ModularPromptModule = ModularPromptModule;" + } + ], + "production": [ + { + "file": "Promptmodules/prompt-manager.js", + "line": 125, + "surface": "shared-renderer", + "snippet": "if (window.ModularPromptModule) {" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 126, + "surface": "shared-renderer", + "snippet": "this.modularModule = new window.ModularPromptModule({" + } + ], + "tests": [] + }, + "nextUiApps": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/next-ui-apps.js", + "line": 40, + "syntax": "dot-assignment", + "snippet": "window.nextUiApps = Object.freeze({ register, unregister, get, list });" + } + ], + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 209, + "surface": "internal-app", + "snippet": "const app = window.nextUiApps?.get(appId);" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 485, + "surface": "internal-app", + "snippet": "if (!window.nextUiApps?.get(descriptor.id)) {" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 791, + "surface": "internal-app", + "snippet": "getInternalApps: () => window.nextUiApps?.list?.() || []," + } + ], + "tests": [] + }, + "notificationRenderer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/notificationRenderer.js", + "line": 604, + "syntax": "dot-assignment", + "snippet": "window.notificationRenderer = {" + } + ], + "production": [ + { + "file": "modules/global-settings-manager.js", + "line": 331, + "surface": "shared-renderer", + "snippet": "if (window.notificationRenderer) window.notificationRenderer.updateVCPLogStatus({ status: 'error', message: 'VCPLog未配置' }, document.getElementById('vcpLogConnectionStatus'));" + }, + { + "file": "modules/global-settings-manager.js", + "line": 331, + "surface": "shared-renderer", + "snippet": "if (window.notificationRenderer) window.notificationRenderer.updateVCPLogStatus({ status: 'error', message: 'VCPLog未配置' }, document.getElementById('vcpLogConnectionStatus'));" + }, + { + "file": "modules/mainChatCommands.js", + "line": 89, + "surface": "main-window", + "snippet": "return capability('notificationRenderer', () => window.notificationRenderer)?.clearPersistentNotifications?.()" + }, + { + "file": "renderer.js", + "line": 233, + "surface": "main-window", + "snippet": "notificationRenderer: window.notificationRenderer," + }, + { + "file": "renderer.js", + "line": 253, + "surface": "main-window", + "snippet": "window.notificationRenderer?.configureCapabilities?.({" + }, + { + "file": "renderer.js", + "line": 392, + "surface": "main-window", + "snippet": "window.notificationRenderer?.configureCapabilities?.({" + }, + { + "file": "renderer.js", + "line": 649, + "surface": "main-window", + "snippet": "consumeLogStatus: statusUpdate => window.notificationRenderer?.updateVCPLogStatus(statusUpdate, vcpLogConnectionStatusDiv)," + }, + { + "file": "renderer.js", + "line": 651, + "surface": "main-window", + "snippet": "if (!window.notificationRenderer) return;" + }, + { + "file": "renderer.js", + "line": 662, + "surface": "main-window", + "snippet": "window.notificationRenderer.renderVCPLogNotification(logData, null, notificationsListUl, themeColors);" + }, + { + "file": "renderer.js", + "line": 792, + "surface": "main-window", + "snippet": "notificationRenderer: window.notificationRenderer," + } + ], + "tests": [] + }, + "onload": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/renderer/contentProcessor.js", + "line": 789, + "syntax": "dot-assignment", + "snippet": "window.onload = () => {" + } + ], + "production": [], + "tests": [] + }, + "onmousemove": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Memomodules/memo-graph.js", + "line": 302, + "syntax": "dot-assignment", + "snippet": "window.onmousemove = (e) => {" + } + ], + "production": [], + "tests": [] + }, + "onmouseup": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Memomodules/memo-graph.js", + "line": 324, + "syntax": "dot-assignment", + "snippet": "window.onmouseup = () => {" + } + ], + "production": [], + "tests": [] + }, + "openAgentModalByIndex": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 505, + "syntax": "dot-assignment", + "snippet": "window.openAgentModalByIndex = (index) => {" + } + ], + "production": [], + "tests": [] + }, + "openComfyUISettings": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1720, + "syntax": "dot-assignment", + "snippet": "window.openComfyUISettings = openComfyUISettings;" + } + ], + "production": [], + "tests": [] + }, + "openDelegationModal": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 764, + "syntax": "dot-assignment", + "snippet": "window.openDelegationModal = async (delegationId) => {" + } + ], + "production": [], + "tests": [] + }, + "openPluginModal": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "PluginManagerModules/plugin-manager.js", + "line": 326, + "syntax": "dot-assignment", + "snippet": "window.openPluginModal = function openPluginModal(folderName) {" + } + ], + "production": [ + { + "file": "PluginManagerModules/plugin-manager.js", + "line": 409, + "surface": "shared-renderer", + "snippet": "if (reopened) window.openPluginModal(reopened.folderName);" + }, + { + "file": "PluginManagerModules/plugin-manager.js", + "line": 460, + "surface": "shared-renderer", + "snippet": "if (reopened) window.openPluginModal(reopened.folderName);" + } + ], + "tests": [] + }, + "openTaskModalByIndex": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 997, + "syntax": "dot-assignment", + "snippet": "window.openTaskModalByIndex = (index) => {" + } + ], + "production": [], + "tests": [] + }, + "openWorkflowEditor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1722, + "syntax": "dot-assignment", + "snippet": "window.openWorkflowEditor = openWorkflowEditor;" + } + ], + "production": [], + "tests": [] + }, + "OriginalPromptModule": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Promptmodules/original-prompt-module.js", + "line": 101, + "syntax": "dot-assignment", + "snippet": "window.OriginalPromptModule = OriginalPromptModule;" + } + ], + "production": [ + { + "file": "Promptmodules/prompt-manager.js", + "line": 119, + "surface": "shared-renderer", + "snippet": "if (window.OriginalPromptModule) {" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 120, + "surface": "shared-renderer", + "snippet": "this.originalModule = new window.OriginalPromptModule({" + } + ], + "tests": [] + }, + "PresetPromptModule": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Promptmodules/preset-prompt-module.js", + "line": 362, + "syntax": "dot-assignment", + "snippet": "window.PresetPromptModule = PresetPromptModule;" + } + ], + "production": [ + { + "file": "Promptmodules/prompt-manager.js", + "line": 131, + "surface": "shared-renderer", + "snippet": "if (window.PresetPromptModule) {" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 132, + "surface": "shared-renderer", + "snippet": "this.presetModule = new window.PresetPromptModule({" + } + ], + "tests": [] + }, + "pretextBridge": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/renderer/pretext-bridge.js", + "line": 13, + "syntax": "dot-assignment", + "snippet": "window.pretextBridge = { isReady: () => false };" + }, + { + "file": "modules/renderer/pretext-bridge.js", + "line": 189, + "syntax": "dot-assignment", + "snippet": "window.pretextBridge = {" + } + ], + "production": [ + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 319, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && widgetData.contentBuffer && widgetId) {" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 319, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && widgetData.contentBuffer && widgetId) {" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 323, + "surface": "shared-renderer", + "snippet": "const estimatedHeight = window.pretextBridge.estimateHeight(widgetId, plainText, 'widget', currentWidth);" + }, + { + "file": "Memomodules/memo.js", + "line": 99, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Memomodules/memo.js", + "line": 99, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Memomodules/memo.js", + "line": 100, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "Memomodules/memo.js", + "line": 791, + "surface": "shared-renderer", + "snippet": "if (!window.pretextBridge || !window.pretextBridge.isReady()) return;" + }, + { + "file": "Memomodules/memo.js", + "line": 791, + "surface": "shared-renderer", + "snippet": "if (!window.pretextBridge || !window.pretextBridge.isReady()) return;" + }, + { + "file": "Memomodules/memo.js", + "line": 809, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(memoId, text, 'memo', width);" + }, + { + "file": "Memomodules/memo.js", + "line": 912, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && currentMemo) {" + }, + { + "file": "Memomodules/memo.js", + "line": 912, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && currentMemo) {" + }, + { + "file": "Memomodules/memo.js", + "line": 915, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight('memo-preview-' + currentMemo.file, content, 'memo', previewWidth);" + }, + { + "file": "Notemodules/notes.js", + "line": 103, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 103, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 105, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth)" + }, + { + "file": "Notemodules/notes.js", + "line": 134, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge?.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 135, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "Notemodules/notes.js", + "line": 630, + "surface": "shared-renderer", + "snippet": "window.pretextBridge &&" + }, + { + "file": "Notemodules/notes.js", + "line": 631, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.isReady() &&" + }, + { + "file": "Notemodules/notes.js", + "line": 635, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(" + }, + { + "file": "Notemodules/notes.js", + "line": 2530, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2530, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2531, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "Notemodules/notes.js", + "line": 2561, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2561, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2562, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "Notemodules/notes.js", + "line": 2644, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2644, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "Notemodules/notes.js", + "line": 2645, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3271, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3271, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3273, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3427, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3427, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3429, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3479, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3479, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3481, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3577, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3577, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3578, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.recalculateAll(window.innerWidth);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 311, + "surface": "voice-chat", + "snippet": "pretextBridge: window.pretextBridge," + }, + { + "file": "modules/renderer/pretext-bridge.js", + "line": 205, + "surface": "shared-renderer", + "snippet": "console.log('[pretext-bridge] Bridge ready. API available at window.pretextBridge');" + }, + { + "file": "modules/renderer/visibilityOptimizer.js", + "line": 312, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && typeof window.pretextBridge.rememberHeight === 'function' && messageId) {" + }, + { + "file": "modules/renderer/visibilityOptimizer.js", + "line": 312, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && typeof window.pretextBridge.rememberHeight === 'function' && messageId) {" + }, + { + "file": "modules/renderer/visibilityOptimizer.js", + "line": 314, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.rememberHeight(messageId, height);" + }, + { + "file": "modules/text-viewer.js", + "line": 1872, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "modules/text-viewer.js", + "line": 1872, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady()) {" + }, + { + "file": "modules/text-viewer.js", + "line": 1875, + "surface": "shared-renderer", + "snippet": "window.pretextBridge.estimateHeight(scopeId, originalRawContent, 'viewer', containerWidth);" + }, + { + "file": "modules/text-viewer.js", + "line": 2204, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && scopeId) {" + }, + { + "file": "modules/text-viewer.js", + "line": 2204, + "surface": "shared-renderer", + "snippet": "if (window.pretextBridge && window.pretextBridge.isReady() && scopeId) {" + }, + { + "file": "modules/text-viewer.js", + "line": 2206, + "surface": "shared-renderer", + "snippet": "const updates = window.pretextBridge.recalculateAll(containerWidth);" + }, + { + "file": "renderer.js", + "line": 238, + "surface": "main-window", + "snippet": "getPretextBridge: () => window.pretextBridge," + }, + { + "file": "renderer.js", + "line": 272, + "surface": "main-window", + "snippet": "pretextBridge: window.pretextBridge," + }, + { + "file": "renderer.js", + "line": 318, + "surface": "main-window", + "snippet": "const surfacePretextBridge = createSurfacePretextBridge(window.pretextBridge);" + }, + { + "file": "renderer.js", + "line": 532, + "surface": "main-window", + "snippet": "pretextBridge: window.pretextBridge," + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 235, + "surface": "rust-assistant", + "snippet": "pretextBridge: window.pretextBridge," + } + ], + "tests": [] + }, + "PromptManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Promptmodules/prompt-manager.js", + "line": 554, + "syntax": "dot-assignment", + "snippet": "window.PromptManager = PromptManager;" + } + ], + "production": [ + { + "file": "modules/settingsManager.js", + "line": 179, + "surface": "shared-renderer", + "snippet": "if (systemPromptContainer && window.PromptManager) {" + }, + { + "file": "modules/settingsManager.js", + "line": 181, + "surface": "shared-renderer", + "snippet": "promptManager = new window.PromptManager();" + }, + { + "file": "modules/settingsManager.js", + "line": 1038, + "surface": "shared-renderer", + "snippet": "if (systemPromptContainer && window.PromptManager && !promptManager) {" + }, + { + "file": "modules/settingsManager.js", + "line": 1040, + "surface": "shared-renderer", + "snippet": "promptManager = new window.PromptManager();" + } + ], + "tests": [] + }, + "refreshToolGrid": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1571, + "syntax": "dot-assignment", + "snippet": "window.refreshToolGrid = async () => {" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 680, + "surface": "shared-renderer", + "snippet": "if (window.refreshToolGrid) await window.refreshToolGrid();" + }, + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 680, + "surface": "shared-renderer", + "snippet": "if (window.refreshToolGrid) await window.refreshToolGrid();" + }, + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 840, + "surface": "shared-renderer", + "snippet": "if (window.refreshToolGrid) {" + }, + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 841, + "surface": "shared-renderer", + "snippet": "await window.refreshToolGrid();" + }, + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 1175, + "surface": "shared-renderer", + "snippet": "if (window.refreshToolGrid) await window.refreshToolGrid();" + }, + { + "file": "VCPHumanToolBox/renderer_modules/tool-manager.js", + "line": 1175, + "surface": "shared-renderer", + "snippet": "if (window.refreshToolGrid) await window.refreshToolGrid();" + } + ], + "tests": [] + }, + "ScriptoriumAgent": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 840, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumAgent = agentPort;" + } + ], + "production": [ + { + "file": "modules/services/scriptoriumAgentControlService.js", + "line": 155, + "surface": "shared-renderer", + "snippet": "'Boolean(window.ScriptoriumAgent && window.ScriptoriumAgent.current)'," + }, + { + "file": "modules/services/scriptoriumAgentControlService.js", + "line": 155, + "surface": "shared-renderer", + "snippet": "'Boolean(window.ScriptoriumAgent && window.ScriptoriumAgent.current)'," + } + ], + "tests": [] + }, + "ScriptoriumAgentPort": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-agent-port.js", + "line": 1586, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumAgentPort = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 44, + "surface": "shared-renderer", + "snippet": "agentPort: window.ScriptoriumAgentPort," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 814, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumAgentPort.createAgentController({" + } + ], + "tests": [] + }, + "ScriptoriumDeckAdapter": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-adapter.js", + "line": 387, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDeckAdapter = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 21, + "surface": "shared-renderer", + "snippet": "deckAdapter: window.ScriptoriumDeckAdapter," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 309, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumDeckAdapter.createDeckAdapter({" + } + ], + "tests": [] + }, + "ScriptoriumDeckEditor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-editor.js", + "line": 837, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDeckEditor = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 24, + "surface": "shared-renderer", + "snippet": "deckEditor: window.ScriptoriumDeckEditor," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 368, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumDeckEditor.createDeckEditor({" + } + ], + "tests": [] + }, + "ScriptoriumDeckExport": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 617, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDeckExport = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 35, + "surface": "shared-renderer", + "snippet": "deckExport: window.ScriptoriumDeckExport," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 432, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumDeckExport.createDeckExporter({" + } + ], + "tests": [] + }, + "ScriptoriumDeckRenderer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-renderer.js", + "line": 287, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDeckRenderer = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 31, + "surface": "shared-renderer", + "snippet": "deckRenderer: window.ScriptoriumDeckRenderer," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 405, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumDeckRenderer.createDeckRenderer({" + } + ], + "tests": [] + }, + "ScriptoriumDocumentStore": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-document-store.js", + "line": 307, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDocumentStore = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 19, + "surface": "shared-renderer", + "snippet": "documentStore: window.ScriptoriumDocumentStore," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 112, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumDocumentStore.createDocumentStore({" + } + ], + "tests": [] + }, + "ScriptoriumDomSelection": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-dom-selection.js", + "line": 308, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumDomSelection = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 22, + "surface": "shared-renderer", + "snippet": "selection: window.ScriptoriumDomSelection," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 332, + "surface": "shared-renderer", + "snippet": "selectionPrimitives: window.ScriptoriumDomSelection," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 371, + "surface": "shared-renderer", + "snippet": "selectionPrimitives: window.ScriptoriumDomSelection," + } + ], + "tests": [] + }, + "ScriptoriumEditHistory": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-edit-history.js", + "line": 244, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumEditHistory = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 33, + "surface": "shared-renderer", + "snippet": "history: window.ScriptoriumEditHistory," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 218, + "surface": "shared-renderer", + "snippet": "const historyPort = window.ScriptoriumEditHistory.createEditHistory({" + } + ], + "tests": [] + }, + "ScriptoriumExport": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-export.js", + "line": 160, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumExport = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 36, + "surface": "shared-renderer", + "snippet": "exportController: window.ScriptoriumExport," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 660, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumExport.createExportController({" + } + ], + "tests": [] + }, + "ScriptoriumExportResources": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-export-resources.js", + "line": 216, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumExportResources = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 664, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumExportResources," + } + ], + "tests": [] + }, + "ScriptoriumFind": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-find.js", + "line": 344, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFind = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 38, + "surface": "shared-renderer", + "snippet": "find: window.ScriptoriumFind," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 684, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFind.createFindController({" + } + ], + "tests": [] + }, + "ScriptoriumFlowAdapter": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-flow-adapter.js", + "line": 304, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFlowAdapter = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 20, + "surface": "shared-renderer", + "snippet": "flowAdapter: window.ScriptoriumFlowAdapter," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 300, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFlowAdapter.createFlowAdapter({" + } + ], + "tests": [] + }, + "ScriptoriumFlowEditor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-flow-editor.js", + "line": 3693, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFlowEditor = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 23, + "surface": "shared-renderer", + "snippet": "flowEditor: window.ScriptoriumFlowEditor," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 329, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFlowEditor.createFlowEditor({" + } + ], + "tests": [] + }, + "ScriptoriumFlowExport": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-flow-export.js", + "line": 262, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFlowExport = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 34, + "surface": "shared-renderer", + "snippet": "flowExport: window.ScriptoriumFlowExport," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 425, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFlowExport.createFlowExporter({" + } + ], + "tests": [] + }, + "ScriptoriumFlowRenderer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-flow-renderer.js", + "line": 648, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFlowRenderer = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 30, + "surface": "shared-renderer", + "snippet": "flowRenderer: window.ScriptoriumFlowRenderer," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 392, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFlowRenderer.createFlowRenderer({" + } + ], + "tests": [] + }, + "ScriptoriumFormatting": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-formatting.js", + "line": 413, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumFormatting = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 25, + "surface": "shared-renderer", + "snippet": "formatting: window.ScriptoriumFormatting," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 712, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumFormatting.createFormattingController({" + } + ], + "tests": [] + }, + "ScriptoriumLineageStore": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-lineage-store.js", + "line": 212, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumLineageStore = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 41, + "surface": "shared-renderer", + "snippet": "lineageStore: window.ScriptoriumLineageStore," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 230, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumLineageStore.createLineageStore({" + } + ], + "tests": [] + }, + "ScriptoriumLineageUi": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-lineage-ui.js", + "line": 827, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumLineageUi = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 42, + "surface": "shared-renderer", + "snippet": "lineageUi: window.ScriptoriumLineageUi," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 787, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumLineageUi.createLineageUiController({" + } + ], + "tests": [] + }, + "ScriptoriumMedia": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-media.js", + "line": 422, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumMedia = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 37, + "surface": "shared-renderer", + "snippet": "media: window.ScriptoriumMedia," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 674, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumMedia.createMediaController({" + } + ], + "tests": [] + }, + "ScriptoriumNavigation": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-navigation.js", + "line": 327, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumNavigation = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 39, + "surface": "shared-renderer", + "snippet": "navigation: window.ScriptoriumNavigation," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 691, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumNavigation" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 694, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumNavigation" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 701, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumNavigation.createNavigationController({" + } + ], + "tests": [] + }, + "ScriptoriumNetworkFonts": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-network-fonts.js", + "line": 298, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumNetworkFonts = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 46, + "surface": "shared-renderer", + "snippet": "networkFonts: window.ScriptoriumNetworkFonts," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 610, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumNetworkFonts.createNetworkFontController({" + } + ], + "tests": [] + }, + "ScriptoriumObjects": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-objects.js", + "line": 1403, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumObjects = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 480, + "surface": "shared-renderer", + "snippet": "const result = window.ScriptoriumObjects.applyMutationToSource(" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 497, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumObjects.normalizeObjectNode(" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 843, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumObjects.createObjectController({" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 863, + "surface": "shared-renderer", + "snippet": "objects: window.ScriptoriumObjects," + }, + { + "file": "ScriptoriumModules/vdoc-svg-asset-library.js", + "line": 65, + "surface": "shared-renderer", + "snippet": "const sanitizer = window.ScriptoriumObjects?.sanitizeSvgSource;" + } + ], + "tests": [] + }, + "ScriptoriumPrDiff": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-pr-diff.js", + "line": 399, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumPrDiff = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 43, + "surface": "shared-renderer", + "snippet": "prDiff: window.ScriptoriumPrDiff," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 778, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumPrDiff.createPrDiffController({" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 823, + "surface": "shared-renderer", + "snippet": "prDiff: window.ScriptoriumPrDiff," + } + ], + "tests": [] + }, + "ScriptoriumPretext": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-pretext-bridge.js", + "line": 144, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumPretext = Object.freeze({" + } + ], + "production": [], + "tests": [] + }, + "ScriptoriumProgrammableContent": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-programmable-content.js", + "line": 398, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumProgrammableContent = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 6, + "surface": "shared-renderer", + "snippet": "const policy = context.policy || window.ScriptoriumProgrammableContent;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 76, + "surface": "shared-renderer", + "snippet": "const policy = window.ScriptoriumProgrammableContent;" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 387, + "surface": "shared-renderer", + "snippet": "const policy = window.ScriptoriumProgrammableContent;" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 822, + "surface": "shared-renderer", + "snippet": "programmableContent: window.ScriptoriumProgrammableContent," + } + ], + "tests": [] + }, + "ScriptoriumRenderCoordinator": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-render-coordinator.js", + "line": 312, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumRenderCoordinator = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 32, + "surface": "shared-renderer", + "snippet": "renderCoordinator: window.ScriptoriumRenderCoordinator," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 597, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumRenderCoordinator.createRenderCoordinator({" + } + ], + "tests": [] + }, + "ScriptoriumRenderedText": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-rendered-text.js", + "line": 914, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumRenderedText = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 29, + "surface": "shared-renderer", + "snippet": "renderedText: window.ScriptoriumRenderedText," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 236, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumRenderedText.createRenderedTextController({" + } + ], + "tests": [] + }, + "ScriptoriumRenderPrimitives": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-render-primitives.js", + "line": 558, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumRenderPrimitives = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 26, + "surface": "shared-renderer", + "snippet": "primitives: window.ScriptoriumRenderPrimitives," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 291, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumRenderPrimitives.createRenderPrimitives({" + } + ], + "tests": [] + }, + "ScriptoriumRuntime": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 569, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumRuntime = Object.freeze({" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 595, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumRuntime = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 45, + "surface": "shared-renderer", + "snippet": "runtime: window.ScriptoriumRuntime," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 245, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumRuntime.createRuntimeController({" + } + ], + "tests": [] + }, + "ScriptoriumSession": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-session.js", + "line": 461, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumSession = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 49, + "surface": "shared-renderer", + "snippet": "session: window.ScriptoriumSession," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 755, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumSession.createSessionController({" + } + ], + "tests": [] + }, + "ScriptoriumSettings": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-settings.js", + "line": 119, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumSettings = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 18, + "surface": "shared-renderer", + "snippet": "settings: window.ScriptoriumSettings," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 109, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumSettings.createSettingsStore();" + } + ], + "tests": [] + }, + "ScriptoriumShell": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-shell.js", + "line": 759, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumShell = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 50, + "surface": "shared-renderer", + "snippet": "shell: window.ScriptoriumShell," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 559, + "surface": "shared-renderer", + "snippet": "shell = window.ScriptoriumShell.createShell({" + } + ], + "tests": [] + }, + "ScriptoriumSourceEditor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-source-editor.js", + "line": 557, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumSourceEditor = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 47, + "surface": "shared-renderer", + "snippet": "sourceEditor: window.ScriptoriumSourceEditor," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 619, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumSourceEditor.createSourceEditorController({" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 646, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumSourceEditor.createSourceEditorController({" + } + ], + "tests": [] + }, + "ScriptoriumStyleUi": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-style-ui.js", + "line": 421, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumStyleUi = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 40, + "surface": "shared-renderer", + "snippet": "styleUi: window.ScriptoriumStyleUi," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 720, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumStyleUi.createStyleUiController({" + } + ], + "tests": [] + }, + "ScriptoriumSvgAssets": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-svg-assets.js", + "line": 770, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumSvgAssets = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 28, + "surface": "shared-renderer", + "snippet": "svgAssets: window.ScriptoriumSvgAssets," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 860, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumSvgAssets.createSvgAssetController({" + } + ], + "tests": [] + }, + "ScriptoriumVisibility": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-visibility.js", + "line": 244, + "syntax": "dot-assignment", + "snippet": "window.ScriptoriumVisibility = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 205, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility?.registerCanvas?.(" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 442, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.resume(entry.target);" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 444, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.pause(entry.target);" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 200, + "surface": "shared-renderer", + "snippet": "const unregisterVisibility = window.ScriptoriumVisibility?.registerCanvas(" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 519, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.resume(entry.target);" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 521, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.pause(entry.target);" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 253, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.pause(surface);" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 258, + "surface": "shared-renderer", + "snippet": "window.ScriptoriumVisibility.resume(surface);" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 264, + "surface": "shared-renderer", + "snippet": "&& window.ScriptoriumVisibility.isPaused(surface)" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 270, + "surface": "shared-renderer", + "snippet": "const observer = window.ScriptoriumVisibility.observePages(" + } + ], + "tests": [] + }, + "sendVcpLogMessageFromObserver": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "RAGmodules/rag-observer-config.js", + "line": 304, + "syntax": "dot-assignment", + "snippet": "window.sendVcpLogMessageFromObserver = (data) => config.sendVcpLogMessage(data);" + } + ], + "production": [ + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3325, + "surface": "shared-renderer", + "snippet": "if (typeof window.sendVcpLogMessageFromObserver === \"function\") {" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3326, + "surface": "shared-renderer", + "snippet": "sendOk = window.sendVcpLogMessageFromObserver(payload) === true;" + } + ], + "tests": [] + }, + "setNotificationChannelEnabledFromObserver": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "RAGmodules/rag-observer-config.js", + "line": 305, + "syntax": "dot-assignment", + "snippet": "window.setNotificationChannelEnabledFromObserver = (enabled) => config.setNotificationChannelEnabled(enabled);" + } + ], + "production": [ + { + "file": "RAGmodules/RAG_Observer.html", + "line": 1799, + "surface": "shared-renderer", + "snippet": "typeof window.setNotificationChannelEnabledFromObserver === \"function\"" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 1801, + "surface": "shared-renderer", + "snippet": "window.setNotificationChannelEnabledFromObserver(" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3694, + "surface": "shared-renderer", + "snippet": "typeof window.setNotificationChannelEnabledFromObserver === \"function\"" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3696, + "surface": "shared-renderer", + "snippet": "window.setNotificationChannelEnabledFromObserver(" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3729, + "surface": "shared-renderer", + "snippet": "typeof window.setNotificationChannelEnabledFromObserver ===" + }, + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3732, + "surface": "shared-renderer", + "snippet": "window.setNotificationChannelEnabledFromObserver(false);" + } + ], + "tests": [] + }, + "settingsManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/settingsManager.js", + "line": 2338, + "syntax": "dot-assignment", + "snippet": "window.settingsManager = settingsManager;" + } + ], + "production": [ + { + "file": "Groupmodules/grouprenderer.js", + "line": 528, + "surface": "shared-renderer", + "snippet": "if (window.settingsManager && typeof window.settingsManager.openModelSelectForInput === 'function') {" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 528, + "surface": "shared-renderer", + "snippet": "if (window.settingsManager && typeof window.settingsManager.openModelSelectForInput === 'function') {" + }, + { + "file": "Groupmodules/grouprenderer.js", + "line": 530, + "surface": "shared-renderer", + "snippet": "await window.settingsManager.openModelSelectForInput(groupUnifiedModelInput);" + }, + { + "file": "Promptmodules/preset-prompt-module.js", + "line": 181, + "surface": "shared-renderer", + "snippet": "window.settingsManager &&" + }, + { + "file": "Promptmodules/preset-prompt-module.js", + "line": 182, + "surface": "shared-renderer", + "snippet": "typeof window.settingsManager.triggerAgentSave === 'function'" + }, + { + "file": "Promptmodules/preset-prompt-module.js", + "line": 184, + "surface": "shared-renderer", + "snippet": "await window.settingsManager.triggerAgentSave(targetAgentId);" + }, + { + "file": "modules/uiManager.js", + "line": 466, + "surface": "shared-renderer", + "snippet": "if (settingsManagerCapability || window.settingsManager) {" + }, + { + "file": "modules/uiManager.js", + "line": 475, + "surface": "shared-renderer", + "snippet": "if ((settingsManagerCapability || window.settingsManager)?.reloadAgentSettings) {" + }, + { + "file": "modules/uiManager.js", + "line": 476, + "surface": "shared-renderer", + "snippet": "track((settingsManagerCapability || window.settingsManager).reloadAgentSettings(pendingAgentId));" + }, + { + "file": "modules/uiManager.js", + "line": 481, + "surface": "shared-renderer", + "snippet": "(settingsManagerCapability || window.settingsManager)?.displaySettingsForItem?.();" + }, + { + "file": "renderer.js", + "line": 236, + "surface": "main-window", + "snippet": "getSettingsManager: () => window.settingsManager," + }, + { + "file": "renderer.js", + "line": 257, + "surface": "main-window", + "snippet": "window.settingsManager?.configureCapabilities?.({ settings: mainChatSettingsOwner });" + }, + { + "file": "renderer.js", + "line": 486, + "surface": "main-window", + "snippet": "displaySettingsForItem: () => window.settingsManager ? window.settingsManager.displaySettingsForItem() : console.error('[GroupRenderer] settingsManager not available')," + }, + { + "file": "renderer.js", + "line": 486, + "surface": "main-window", + "snippet": "displaySettingsForItem: () => window.settingsManager ? window.settingsManager.displaySettingsForItem() : console.error('[GroupRenderer] settingsManager not available')," + }, + { + "file": "renderer.js", + "line": 777, + "surface": "main-window", + "snippet": "displaySettingsForItem: () => window.settingsManager.displaySettingsForItem()," + }, + { + "file": "renderer.js", + "line": 803, + "surface": "main-window", + "snippet": "if (window.settingsManager) {" + }, + { + "file": "renderer.js", + "line": 804, + "surface": "main-window", + "snippet": "window.settingsManager.init({" + }, + { + "file": "renderer.js", + "line": 856, + "surface": "main-window", + "snippet": "if (window.settingsManager.prewarmPromptManager) {" + }, + { + "file": "renderer.js", + "line": 857, + "surface": "main-window", + "snippet": "window.settingsManager.prewarmPromptManager();" + }, + { + "file": "renderer.js", + "line": 884, + "surface": "main-window", + "snippet": "settingsManager: window.settingsManager," + }, + { + "file": "renderer.js", + "line": 942, + "surface": "main-window", + "snippet": "settingsManager: window.settingsManager," + }, + { + "file": "renderer.js", + "line": 1094, + "surface": "main-window", + "snippet": "if (window.settingsManager && typeof window.settingsManager.reloadAgentSettings === 'function') {" + }, + { + "file": "renderer.js", + "line": 1094, + "surface": "main-window", + "snippet": "if (window.settingsManager && typeof window.settingsManager.reloadAgentSettings === 'function') {" + }, + { + "file": "renderer.js", + "line": 1095, + "surface": "main-window", + "snippet": "const result = await window.settingsManager.reloadAgentSettings(agentId);" + } + ], + "tests": [] + }, + "startRecognition": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Voicechatmodules/recognizer.html", + "line": 170, + "syntax": "dot-assignment", + "snippet": "window.startRecognition = () => {" + } + ], + "production": [ + { + "file": "Voicechatmodules/recognizer.html", + "line": 163, + "surface": "voice-chat", + "snippet": "window.startRecognition();" + }, + { + "file": "Voicechatmodules/recognizer.html", + "line": 189, + "surface": "voice-chat", + "snippet": "window.startRecognition();" + }, + { + "file": "modules/speechRecognizer.js", + "line": 173, + "surface": "shared-renderer", + "snippet": "await page.evaluate(() => window.startRecognition());" + } + ], + "tests": [] + }, + "startSpectrumAnimation": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "RAGmodules/RAG_Observer.html", + "line": 3584, + "syntax": "dot-assignment", + "snippet": "window.startSpectrumAnimation = function (duration = 3000) {" + } + ], + "production": [ + { + "file": "RAGmodules/rag-observer-config.js", + "line": 92, + "surface": "shared-renderer", + "snippet": "if (window.startSpectrumAnimation) {" + }, + { + "file": "RAGmodules/rag-observer-config.js", + "line": 93, + "surface": "shared-renderer", + "snippet": "window.startSpectrumAnimation(3000);" + }, + { + "file": "RAGmodules/rag-observer-config.js", + "line": 159, + "surface": "shared-renderer", + "snippet": "if (window.startSpectrumAnimation) {" + }, + { + "file": "RAGmodules/rag-observer-config.js", + "line": 160, + "surface": "shared-renderer", + "snippet": "window.startSpectrumAnimation(3000);" + } + ], + "tests": [] + }, + "stopRecognition": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Voicechatmodules/recognizer.html", + "line": 176, + "syntax": "dot-assignment", + "snippet": "window.stopRecognition = () => {" + } + ], + "production": [ + { + "file": "Voicechatmodules/recognizer.html", + "line": 186, + "surface": "voice-chat", + "snippet": "window.stopRecognition();" + }, + { + "file": "modules/speechRecognizer.js", + "line": 197, + "surface": "shared-renderer", + "snippet": "await page.evaluate(() => window.stopRecognition());" + } + ], + "tests": [] + }, + "summarizeTopicFromMessages": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/topicSummarizer.js", + "line": 114, + "syntax": "dot-assignment", + "snippet": "window.summarizeTopicFromMessages = summarizeTopicFromMessages;" + } + ], + "production": [ + { + "file": "Voicechatmodules/voicechat.js", + "line": 147, + "surface": "voice-chat", + "snippet": "if (window.summarizeTopicFromMessages) {" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 149, + "surface": "voice-chat", + "snippet": "const summarizedTitle = await window.summarizeTopicFromMessages(persistedHistory, agentName);" + }, + { + "file": "Voicechatmodules/voicechat.js", + "line": 315, + "surface": "voice-chat", + "snippet": "summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => \"\")," + }, + { + "file": "renderer.js", + "line": 547, + "surface": "main-window", + "snippet": "if (typeof window.summarizeTopicFromMessages === 'function') return window.summarizeTopicFromMessages(messages, agentName);" + }, + { + "file": "renderer.js", + "line": 547, + "surface": "main-window", + "snippet": "if (typeof window.summarizeTopicFromMessages === 'function') return window.summarizeTopicFromMessages(messages, agentName);" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 112, + "surface": "rust-assistant", + "snippet": "if (window.summarizeTopicFromMessages) {" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 114, + "surface": "rust-assistant", + "snippet": "const summarizedTitle = await window.summarizeTopicFromMessages(persistedHistory, agentName);" + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 239, + "surface": "rust-assistant", + "snippet": "summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => \"\")," + } + ], + "tests": [] + }, + "syncAgentQuickSummary": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 534, + "syntax": "dot-assignment", + "snippet": "window.syncAgentQuickSummary = (index, val) => {" + } + ], + "production": [], + "tests": [] + }, + "TavernManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Tavernmodules/tavern-manager.js", + "line": 639, + "syntax": "dot-assignment", + "snippet": "window.TavernManager = TavernManager;" + } + ], + "production": [ + { + "file": "modules/chatManager.js", + "line": 312, + "surface": "shared-renderer", + "snippet": "const manager = window.TavernManager;" + }, + { + "file": "modules/event-listeners.js", + "line": 437, + "surface": "main-window", + "snippet": "if (window.TavernManager && typeof window.TavernManager.togglePopover === 'function') {" + }, + { + "file": "modules/event-listeners.js", + "line": 437, + "surface": "main-window", + "snippet": "if (window.TavernManager && typeof window.TavernManager.togglePopover === 'function') {" + }, + { + "file": "modules/event-listeners.js", + "line": 438, + "surface": "main-window", + "snippet": "window.TavernManager.togglePopover(sendMessageBtn);" + } + ], + "tests": [] + }, + "testConnectionEvent": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_CanvasManager_JSPlumb.js", + "line": 44, + "syntax": "dot-assignment", + "snippet": "window.testConnectionEvent = () => {" + } + ], + "production": [], + "tests": [] + }, + "toggleGlobalScheduler": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1258, + "syntax": "dot-assignment", + "snippet": "window.toggleGlobalScheduler = async (enabled) => {" + } + ], + "production": [], + "tests": [] + }, + "toggleTaskDelegation": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1014, + "syntax": "dot-assignment", + "snippet": "window.toggleTaskDelegation = async (index, delegated) => {" + } + ], + "production": [], + "tests": [] + }, + "toggleTaskEnabled": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1008, + "syntax": "dot-assignment", + "snippet": "window.toggleTaskEnabled = async (index, enabled) => {" + } + ], + "production": [], + "tests": [] + }, + "toolManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1566, + "syntax": "dot-assignment", + "snippet": "window.toolManager = new ToolManager();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1567, + "surface": "shared-renderer", + "snippet": "window.toolManagerUI = new ToolManagerUI(window.toolManager);" + } + ], + "tests": [] + }, + "toolManagerUI": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1567, + "syntax": "dot-assignment", + "snippet": "window.toolManagerUI = new ToolManagerUI(window.toolManager);" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1559, + "surface": "shared-renderer", + "snippet": "await window.toolManagerUI.init('manage-panel');" + } + ], + "tests": [] + }, + "toolManagerUIInitialized": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1560, + "syntax": "dot-assignment", + "snippet": "window.toolManagerUIInitialized = true;" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1568, + "syntax": "dot-assignment", + "snippet": "window.toolManagerUIInitialized = false;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1558, + "surface": "shared-renderer", + "snippet": "if (!window.toolManagerUIInitialized) {" + } + ], + "tests": [] + }, + "topicListManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/topicListManager.js", + "line": 3, + "syntax": "dot-assignment", + "snippet": "window.topicListManager = (() => {" + } + ], + "production": [ + { + "file": "modules/ui-helpers.js", + "line": 463, + "surface": "shared-renderer", + "snippet": "if (window.topicListManager && window.topicListManager.showTopicContextMenu) {" + }, + { + "file": "modules/ui-helpers.js", + "line": 463, + "surface": "shared-renderer", + "snippet": "if (window.topicListManager && window.topicListManager.showTopicContextMenu) {" + }, + { + "file": "modules/ui-helpers.js", + "line": 464, + "surface": "shared-renderer", + "snippet": "window.topicListManager.showTopicContextMenu(event, topicItemElement, itemFullConfig, topic, itemType);" + }, + { + "file": "modules/uiManager.js", + "line": 353, + "surface": "shared-renderer", + "snippet": "window.topicListManager?.loadTopicList?.();" + }, + { + "file": "modules/uiManager.js", + "line": 354, + "surface": "shared-renderer", + "snippet": "window.topicListManager?.setupTopicSearch?.();" + }, + { + "file": "modules/uiManager.js", + "line": 459, + "surface": "shared-renderer", + "snippet": "if (window.topicListManager) {" + }, + { + "file": "modules/uiManager.js", + "line": 460, + "surface": "shared-renderer", + "snippet": "window.topicListManager.loadTopicList(); // This might create/re-render the topic list and search input" + }, + { + "file": "modules/uiManager.js", + "line": 461, + "surface": "shared-renderer", + "snippet": "window.topicListManager.setupTopicSearch(); // Explicitly set up search listeners after the tab is active and list loaded" + }, + { + "file": "renderer.js", + "line": 487, + "surface": "main-window", + "snippet": "loadTopicList: () => window.topicListManager ? window.topicListManager.loadTopicList() : console.error('[GroupRenderer] topicListManager not available')," + }, + { + "file": "renderer.js", + "line": 487, + "surface": "main-window", + "snippet": "loadTopicList: () => window.topicListManager ? window.topicListManager.loadTopicList() : console.error('[GroupRenderer] topicListManager not available')," + }, + { + "file": "renderer.js", + "line": 685, + "surface": "main-window", + "snippet": "await window.topicListManager.loadTopicList();" + }, + { + "file": "renderer.js", + "line": 700, + "surface": "main-window", + "snippet": "if (window.topicListManager) {" + }, + { + "file": "renderer.js", + "line": 701, + "surface": "main-window", + "snippet": "window.topicListManager.init({" + }, + { + "file": "renderer.js", + "line": 755, + "surface": "main-window", + "snippet": "topicListManager: window.topicListManager," + }, + { + "file": "renderer.js", + "line": 784, + "surface": "main-window", + "snippet": "ownedRendererSubscriptions.add({ dispose: () => window.topicListManager.dispose?.() });" + }, + { + "file": "renderer.js", + "line": 944, + "surface": "main-window", + "snippet": "topicListManager: window.topicListManager," + }, + { + "file": "renderer.js", + "line": 989, + "surface": "main-window", + "snippet": "window.topicListManager.setupTopicSearch(); // Ensure this is called after DOM for topic search input is ready" + } + ], + "tests": [ + { + "file": "tests/topic-list-mode-lifecycle.test.js", + "line": 34, + "surface": "test", + "snippet": "window.topicListManager.init({" + }, + { + "file": "tests/topic-list-mode-lifecycle.test.js", + "line": 78, + "surface": "test", + "snippet": "window.topicListManager.init({" + }, + { + "file": "tests/topic-list-mode-lifecycle.test.js", + "line": 85, + "surface": "test", + "snippet": "window.topicListManager.dispose();" + }, + { + "file": "tests/topic-list-mode-lifecycle.test.js", + "line": 88, + "surface": "test", + "snippet": "assert.equal(typeof window.topicListManager.dispose, 'function');" + } + ] + }, + "topTabManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/topTabManager.js", + "line": 4, + "syntax": "dot-assignment", + "snippet": "window.topTabManager = Object.freeze({" + } + ], + "production": [ + { + "file": "modules/mainChatCommands.js", + "line": 45, + "surface": "main-window", + "snippet": "return capability('topTabManager', () => window.topTabManager)?.openCreateDialog?.();" + }, + { + "file": "modules/trayManager.js", + "line": 380, + "surface": "shared-renderer", + "snippet": "if (overlayAcquired) window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/trayManager.js", + "line": 383, + "surface": "shared-renderer", + "snippet": "Promise.resolve(window.topTabManager?.acquireOverlay?.(overlayOwner)).then(() => {" + }, + { + "file": "modules/trayManager.js", + "line": 386, + "surface": "shared-renderer", + "snippet": "window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1241, + "surface": "internal-app", + "snippet": "window.topTabManager?.releaseOverlay?.(owner);" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1243, + "surface": "internal-app", + "snippet": "Promise.resolve(window.topTabManager?.acquireOverlay?.(owner)).catch(error => {" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1261, + "surface": "internal-app", + "snippet": "window.topTabManager?.releaseOverlay?.(owner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 247, + "surface": "internal-app", + "snippet": "await window.topTabManager?.acquireOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 256, + "surface": "internal-app", + "snippet": "window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 259, + "surface": "internal-app", + "snippet": "modalScope?.own(() => window.topTabManager?.releaseOverlay?.(overlayOwner), 'overlay-lease', 'overlay');" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 262, + "surface": "internal-app", + "snippet": "else window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 267, + "surface": "internal-app", + "snippet": "else window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 272, + "surface": "internal-app", + "snippet": "else window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 371, + "surface": "internal-app", + "snippet": "window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 388, + "surface": "internal-app", + "snippet": "else window.topTabManager?.releaseOverlay?.(overlayOwner);" + }, + { + "file": "renderer.js", + "line": 413, + "surface": "main-window", + "snippet": "if (window.topTabManager) {" + }, + { + "file": "renderer.js", + "line": 414, + "surface": "main-window", + "snippet": "window.topTabManager.init();" + }, + { + "file": "renderer.js", + "line": 794, + "surface": "main-window", + "snippet": "topTabManager: window.topTabManager," + } + ], + "tests": [] + }, + "trayManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/trayManager.js", + "line": 449, + "syntax": "dot-assignment", + "snippet": "window.trayManager = trayManager;" + } + ], + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 316, + "surface": "internal-app", + "snippet": "await window.trayManager?.launchApp(app);" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 356, + "surface": "internal-app", + "snippet": "iconSvg: window.trayManager?.getIcon(app.icon)," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 451, + "surface": "internal-app", + "snippet": "const apps = window.trayManager?.getApps?.() || [];" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 492, + "surface": "internal-app", + "snippet": "const app = window.trayManager?.getApps?.().find(candidate => candidate.id === descriptor.id && candidate.embed);" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 790, + "surface": "internal-app", + "snippet": "getExternalApps: () => window.trayManager?.getApps?.() || []," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 792, + "surface": "internal-app", + "snippet": "getIcon: icon => window.trayManager?.getIcon?.(icon) || ''," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 793, + "surface": "internal-app", + "snippet": "openExternal: app => window.trayManager?.launchApp?.(app)," + }, + { + "file": "renderer.js", + "line": 407, + "surface": "main-window", + "snippet": "if (window.trayManager) {" + }, + { + "file": "renderer.js", + "line": 408, + "surface": "main-window", + "snippet": "window.trayManager.init();" + } + ], + "tests": [] + }, + "triggerTaskDirect": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 985, + "syntax": "dot-assignment", + "snippet": "window.triggerTaskDirect = async (taskId) => {" + } + ], + "production": [], + "tests": [] + }, + "uiHelperFunctions": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-helpers.js", + "line": 976, + "syntax": "dot-assignment", + "snippet": "window.uiHelperFunctions = uiHelperFunctions;" + } + ], + "production": [ + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 28, + "surface": "flowlock", + "snippet": "uiHelper: window.uiHelperFunctions," + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 329, + "surface": "flowlock", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 330, + "surface": "flowlock", + "snippet": "window.uiHelperFunctions.showToastNotification('请先选择一个Agent和话题', 'warning');" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 360, + "surface": "flowlock", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 361, + "surface": "flowlock", + "snippet": "window.uiHelperFunctions.showToastNotification('请先选择一个Agent和话题', 'warning');" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 388, + "surface": "flowlock", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 389, + "surface": "flowlock", + "snippet": "window.uiHelperFunctions.showToastNotification('请先选择一个Agent和话题', 'warning');" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 336, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions &&" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 337, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification" + }, + { + "file": "Promptmodules/prompt-manager.js", + "line": 339, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification(" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 239, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 240, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification(" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 401, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 402, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification('顺序已更新并保存', 'success');" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 406, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 407, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification(`保存顺序失败: ${result?.error || '未知错误'}`, 'error');" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 570, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 571, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification('规则已保存', 'success');" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 576, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 577, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification(`保存失败: ${result?.error || '未知错误'}`, 'error');" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 586, + "surface": "shared-renderer", + "snippet": "const confirmFn = (window.uiHelperFunctions && typeof window.uiHelperFunctions.showConfirmDialog === 'function')" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 586, + "surface": "shared-renderer", + "snippet": "const confirmFn = (window.uiHelperFunctions && typeof window.uiHelperFunctions.showConfirmDialog === 'function')" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 587, + "surface": "shared-renderer", + "snippet": "? window.uiHelperFunctions.showConfirmDialog.bind(window.uiHelperFunctions)" + }, + { + "file": "Tavernmodules/tavern-manager.js", + "line": 587, + "surface": "shared-renderer", + "snippet": "? window.uiHelperFunctions.showConfirmDialog.bind(window.uiHelperFunctions)" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 60, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 61, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification(message, type);" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 62, + "surface": "shared-renderer", + "snippet": "} else if (window.uiHelperFunctions?.showToast) {" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 63, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToast(message, type);" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 91, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions) {" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 92, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.openModal(modalId);" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 103, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions) {" + }, + { + "file": "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js", + "line": 104, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.closeModal(modalId);" + }, + { + "file": "modules/chatManager.js", + "line": 211, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.getCompiledRegex) {" + }, + { + "file": "modules/chatManager.js", + "line": 212, + "surface": "shared-renderer", + "snippet": "const compiled = window.uiHelperFunctions.getCompiledRegex(rule.findPattern);" + }, + { + "file": "modules/chatManager.js", + "line": 216, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.regexFromString) {" + }, + { + "file": "modules/chatManager.js", + "line": 217, + "surface": "shared-renderer", + "snippet": "return window.uiHelperFunctions.regexFromString(rule.findPattern);" + }, + { + "file": "modules/mainChatCommands.js", + "line": 26, + "surface": "main-window", + "snippet": "const open = () => capability('uiHelper', () => window.uiHelperFunctions)?.openModal?.('globalSettingsModal');" + }, + { + "file": "modules/mainChatCommands.js", + "line": 49, + "surface": "main-window", + "snippet": "capability('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.(message, type);" + }, + { + "file": "modules/trayManager.js", + "line": 409, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "modules/trayManager.js", + "line": 410, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification('最多只能选择 4 个常用应用', 'warning');" + }, + { + "file": "modules/trayManager.js", + "line": 421, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "modules/trayManager.js", + "line": 422, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification('请至少选择一个应用', 'warning');" + }, + { + "file": "modules/trayManager.js", + "line": 431, + "surface": "shared-renderer", + "snippet": "if (window.uiHelperFunctions?.showToastNotification) {" + }, + { + "file": "modules/trayManager.js", + "line": 432, + "surface": "shared-renderer", + "snippet": "window.uiHelperFunctions.showToastNotification('常用应用设置已保存', 'success');" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1172, + "surface": "internal-app", + "snippet": "service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.('外观与布局已应用。', 'success');" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1181, + "surface": "internal-app", + "snippet": "service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.(" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1194, + "surface": "internal-app", + "snippet": "service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.(`外观与布局保存失败:${error.message}`, 'error');" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1417, + "surface": "internal-app", + "snippet": "service('uiHelper', () => window.uiHelperFunctions)?.openModal?.('globalSettingsModal');" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 740, + "surface": "internal-app", + "snippet": "if (typeof window.uiHelperFunctions?.closeModal !== 'function') return false;" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 741, + "surface": "internal-app", + "snippet": "window.uiHelperFunctions.closeModal('globalSettingsModal');" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 760, + "surface": "internal-app", + "snippet": "filter: value => window.uiHelperFunctions?.filterAgentList?.(value)," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 768, + "surface": "internal-app", + "snippet": "openSettings: () => window.uiHelperFunctions?.openModal?.('globalSettingsModal')," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 784, + "surface": "internal-app", + "snippet": "showToast: (message, variant) => window.uiHelperFunctions?.showToastNotification?.(message, variant)," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 815, + "surface": "internal-app", + "snippet": "showUnavailable: message => window.uiHelperFunctions?.showToastNotification?.(" + }, + { + "file": "modules/uiManager.js", + "line": 290, + "surface": "shared-renderer", + "snippet": "if ((uiHelperCapability || window.uiHelperFunctions)?.openModal) {" + }, + { + "file": "modules/uiManager.js", + "line": 292, + "surface": "shared-renderer", + "snippet": "(uiHelperCapability || window.uiHelperFunctions).openModal('globalSettingsModal');" + }, + { + "file": "renderer.js", + "line": 160, + "surface": "main-window", + "snippet": "const uiHelperFunctions = window.uiHelperFunctions;" + }, + { + "file": "renderer.js", + "line": 1149, + "surface": "main-window", + "snippet": "uiHelperFunctions: window.uiHelperFunctions," + }, + { + "file": "rust_assistant_engine/ui/assistant.js", + "line": 237, + "surface": "rust-assistant", + "snippet": "uiHelper: window.uiHelperFunctions," + } + ], + "tests": [] + }, + "uiManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/uiManager.js", + "line": 582, + "syntax": "dot-assignment", + "snippet": "window.uiManager = uiManager;" + } + ], + "production": [ + { + "file": "modules/itemListManager.js", + "line": 1025, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/itemListManager.js", + "line": 1025, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/itemListManager.js", + "line": 1027, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab('settings');" + }, + { + "file": "modules/itemListManager.js", + "line": 1083, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/itemListManager.js", + "line": 1083, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/itemListManager.js", + "line": 1085, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab('topics');" + }, + { + "file": "modules/mainChatCommands.js", + "line": 37, + "surface": "main-window", + "snippet": "const currentTheme = capability('uiManager', () => window.uiManager)?.getThemeState?.()?.effective || 'light';" + }, + { + "file": "modules/mainChatCommands.js", + "line": 114, + "surface": "main-window", + "snippet": "capability('uiManager', () => window.uiManager)?.switchToTab?.('settings');" + }, + { + "file": "modules/mainChatCommands.js", + "line": 138, + "surface": "main-window", + "snippet": "capability('uiManager', () => window.uiManager)?.switchToTab?.('settings');" + }, + { + "file": "modules/settingsManager.js", + "line": 717, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 717, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 718, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab('agents');" + }, + { + "file": "modules/settingsManager.js", + "line": 739, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 739, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 740, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab('topics');" + }, + { + "file": "modules/settingsManager.js", + "line": 1181, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 1181, + "surface": "shared-renderer", + "snippet": "if (window.uiManager && typeof window.uiManager.switchToTab === 'function') {" + }, + { + "file": "modules/settingsManager.js", + "line": 1182, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab('settings');" + }, + { + "file": "modules/settingsManager.js", + "line": 1196, + "surface": "shared-renderer", + "snippet": "window.uiManager.switchToTab(currentTabName);" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 26, + "surface": "internal-app", + "snippet": "const getUiManager = () => service('uiManager', () => window.uiManager);" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 775, + "surface": "internal-app", + "snippet": "subscribeTheme: (listener, options) => window.uiManager?.subscribeTheme?.(listener, options)," + }, + { + "file": "renderer.js", + "line": 262, + "surface": "main-window", + "snippet": "uiManager: window.uiManager," + }, + { + "file": "renderer.js", + "line": 269, + "surface": "main-window", + "snippet": "getUiManager: () => window.uiManager," + }, + { + "file": "renderer.js", + "line": 501, + "surface": "main-window", + "snippet": "switchToTab: (tab) => window.uiManager ? window.uiManager.switchToTab(tab) : console.error('[GroupRenderer] uiManager not available')," + }, + { + "file": "renderer.js", + "line": 501, + "surface": "main-window", + "snippet": "switchToTab: (tab) => window.uiManager ? window.uiManager.switchToTab(tab) : console.error('[GroupRenderer] uiManager not available')," + }, + { + "file": "renderer.js", + "line": 713, + "surface": "main-window", + "snippet": "uiManager: window.uiManager," + }, + { + "file": "renderer.js", + "line": 789, + "surface": "main-window", + "snippet": "uiManager: window.uiManager," + }, + { + "file": "renderer.js", + "line": 876, + "surface": "main-window", + "snippet": "if (window.uiManager) {" + }, + { + "file": "renderer.js", + "line": 877, + "surface": "main-window", + "snippet": "ownedRendererSubscriptions.add({ dispose: () => window.uiManager.dispose?.() });" + }, + { + "file": "renderer.js", + "line": 878, + "surface": "main-window", + "snippet": "await window.uiManager.init({" + }, + { + "file": "renderer.js", + "line": 943, + "surface": "main-window", + "snippet": "uiManager: window.uiManager," + } + ], + "tests": [] + }, + "updateAAGlobal": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 519, + "syntax": "dot-assignment", + "snippet": "window.updateAAGlobal = (key, val, type) => {" + } + ], + "production": [], + "tests": [] + }, + "updateAgentQuickField": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 529, + "syntax": "dot-assignment", + "snippet": "window.updateAgentQuickField = (index, key, val) => {" + } + ], + "production": [], + "tests": [] + }, + "updateModalAgent": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1187, + "syntax": "dot-assignment", + "snippet": "window.updateModalAgent = (val) => {" + } + ], + "production": [], + "tests": [] + }, + "updateModalSchedule": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1206, + "syntax": "dot-assignment", + "snippet": "window.updateModalSchedule = (val) => {" + } + ], + "production": [], + "tests": [] + }, + "updateModalType": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Agenttaskmodules/task.js", + "line": 1177, + "syntax": "dot-assignment", + "snippet": "window.updateModalType = (val) => {" + } + ], + "production": [], + "tests": [] + }, + "VCPAppearance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/appearance-engine.js", + "line": 250, + "syntax": "dot-assignment", + "snippet": "window.VCPAppearance = Object.freeze({" + } + ], + "production": [ + { + "file": "modules/global-settings-manager.js", + "line": 44, + "surface": "shared-renderer", + "snippet": "getAppearance = () => window.VCPAppearance" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 25, + "surface": "internal-app", + "snippet": "const getAppearance = () => service('appearance', () => window.VCPAppearance);" + }, + { + "file": "renderer.js", + "line": 237, + "surface": "main-window", + "snippet": "getAppearance: () => window.VCPAppearance," + }, + { + "file": "renderer.js", + "line": 261, + "surface": "main-window", + "snippet": "appearance: window.VCPAppearance," + }, + { + "file": "renderer.js", + "line": 954, + "surface": "main-window", + "snippet": "getAppearance: () => window.VCPAppearance," + } + ], + "tests": [] + }, + "VCPAppearanceStudio": { + "classification": "supported-public-facade", + "definitions": [ + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1526, + "syntax": "define-property", + "snippet": "Object.defineProperty(window, 'VCPAppearanceStudio', { value: Object.freeze({" + } + ], + "production": [ + { + "file": "modules/mainChatCommands.js", + "line": 39, + "surface": "main-window", + "snippet": "if (!capability('appearanceStudio', () => window.VCPAppearanceStudio)?.setThemeMode?.(nextTheme, { source: 'main-chat-command' })) {" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 769, + "surface": "internal-app", + "snippet": "openAppearance: trigger => window.VCPAppearanceStudio?.open?.({ trigger })," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 771, + "surface": "internal-app", + "snippet": "setThemeMode: mode => window.VCPAppearanceStudio?.setThemeMode?.(mode, { source: 'account-menu' })," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 773, + "surface": "internal-app", + "snippet": "syncAppearance: () => window.VCPAppearanceStudio?.syncAccountMenuValue?.()," + }, + { + "file": "renderer.js", + "line": 793, + "surface": "main-window", + "snippet": "appearanceStudio: window.VCPAppearanceStudio," + } + ], + "tests": [] + }, + "VCPDeck": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 496, + "syntax": "dot-assignment", + "snippet": "window.VCPDeck = Object.freeze({" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 493, + "syntax": "dot-assignment", + "snippet": "window.VCPDeck = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 149, + "surface": "shared-renderer", + "snippet": "window.VCPDeck," + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 507, + "surface": "shared-renderer", + "snippet": "if (action === 'next') window.VCPDeck.next();" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 508, + "surface": "shared-renderer", + "snippet": "else if (action === 'previous') window.VCPDeck.previous();" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 516, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.next();" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 519, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.previous();" + }, + { + "file": "ScriptoriumModules/scriptorium-deck-export.js", + "line": 530, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.goTo(initialSlide);" + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.js", + "line": 376, + "surface": "shared-renderer", + "snippet": "deck: window.VCPDeck || null," + }, + { + "file": "ScriptoriumModules/scriptorium-runtime.origin.js", + "line": 366, + "surface": "shared-renderer", + "snippet": "deck: window.VCPDeck || null," + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 504, + "surface": "shared-renderer", + "snippet": "if (action === 'next') window.VCPDeck.next();" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 505, + "surface": "shared-renderer", + "snippet": "else if (action === 'previous') window.VCPDeck.previous();" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 513, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.next();" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 516, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.previous();" + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 594, + "surface": "shared-renderer", + "snippet": "window.VCPDeck," + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 693, + "surface": "shared-renderer", + "snippet": "window.VCPDeck," + }, + { + "file": "VCP文坊超级引擎-四重动画实证.html", + "line": 724, + "surface": "shared-renderer", + "snippet": "window.VCPDeck.goTo(initialSlide);" + } + ], + "tests": [] + }, + "VCPDesktop": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "Desktopmodules/api/desktopMetrics.js", + "line": 268, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 464, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/api/vcpProxy.js", + "line": 184, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 413, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/musicWidget.js", + "line": 178, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/newsWidget.js", + "line": 249, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "line": 240, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 439, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/translateWidget.js", + "line": 313, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 634, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/weatherWidget.js", + "line": 183, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/dragSystem.js", + "line": 97, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 147, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/state.js", + "line": 101, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/statusIndicator.js", + "line": 55, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 510, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/theme.js", + "line": 55, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/visibilityFreezer.js", + "line": 392, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 346, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 727, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/core/zIndexManager.js", + "line": 146, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 148, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/favorites/favoritesManager.js", + "line": 202, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/favorites/thumbnail.js", + "line": 117, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 493, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1925, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 834, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/iconPicker.js", + "line": 312, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/saveModal.js", + "line": 87, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 1106, + "syntax": "dot-assignment", + "snippet": "window.VCPDesktop = window.VCPDesktop || {};" + } + ], + "production": [ + { + "file": "Desktopmodules/api/desktopMetrics.js", + "line": 269, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.metrics = {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 5, + "surface": "shared-renderer", + "snippet": "const { state, status, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 88, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.vchatApps?.VCHAT_APPS) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 89, + "surface": "shared-renderer", + "snippet": "for (const app of window.VCPDesktop.vchatApps.VCHAT_APPS) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 99, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.vchatApps?.SYSTEM_TOOLS) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 100, + "surface": "shared-renderer", + "snippet": "for (const tool of window.VCPDesktop.vchatApps.SYSTEM_TOOLS) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 161, + "surface": "shared-renderer", + "snippet": "if (builtinKey && window.VCPDesktop.metricWidgets?.spawn) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 162, + "surface": "shared-renderer", + "snippet": "const spawnResult = window.VCPDesktop.metricWidgets.spawn(builtinKey, {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 208, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop?.sidebar?.refresh) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 209, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar.refresh();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 210, + "surface": "shared-renderer", + "snippet": "} else if (window.VCPDesktop?.favorites?.loadList) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 211, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.loadList();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 231, + "surface": "shared-renderer", + "snippet": "if (!window.VCPDesktop?.styleAutomation) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 234, + "surface": "shared-renderer", + "snippet": "const statusResult = await window.VCPDesktop.styleAutomation.setConfigPatch(payload.configPatch, {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 248, + "surface": "shared-renderer", + "snippet": "if (!window.VCPDesktop?.styleAutomation) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 255, + "surface": "shared-renderer", + "snippet": "status: window.VCPDesktop.styleAutomation.getStatus()," + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 326, + "surface": "shared-renderer", + "snippet": "if (widgetData.savedId === savedId && window.VCPDesktop?.favorites?.refresh) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 327, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.refresh(widgetId);" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 332, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 337, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', message);" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 338, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 339, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 361, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.wallpaper) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 362, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.wallpaper.apply(wallpaperConfig);" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 365, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.globalSettings?.save) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 366, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.globalSettings.save();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 419, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop?.sidebar?.refresh) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 420, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar.refresh();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 421, + "surface": "shared-renderer", + "snippet": "} else if (window.VCPDesktop?.favorites?.loadList) {" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 422, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.loadList();" + }, + { + "file": "Desktopmodules/api/ipcBridge.js", + "line": 465, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.ipc = {" + }, + { + "file": "Desktopmodules/api/vcpProxy.js", + "line": 185, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.vcpApi = {" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 11, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 233, + "surface": "shared-renderer", + "snippet": "var D = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 362, + "surface": "shared-renderer", + "snippet": "if (existing && window.VCPDesktop.zIndex) {" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 363, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.zIndex.bringToFront(widgetId);" + }, + { + "file": "Desktopmodules/builtinWidgets/appTrayWidget.js", + "line": 414, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinAppTray = {" + }, + { + "file": "Desktopmodules/builtinWidgets/musicWidget.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/musicWidget.js", + "line": 179, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinMusic = {" + }, + { + "file": "Desktopmodules/builtinWidgets/newsWidget.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/newsWidget.js", + "line": 250, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinNews = {" + }, + { + "file": "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "line": 10, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "line": 93, + "surface": "shared-renderer", + "snippet": "var pm = window.VCPDesktop.performanceManager;" + }, + { + "file": "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "line": 164, + "surface": "shared-renderer", + "snippet": "var widgetData = window.VCPDesktop.state.widgets.get(w.id);" + }, + { + "file": "Desktopmodules/builtinWidgets/performanceMonitorWidget.js", + "line": 241, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinPerformanceMonitor = {" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 27, + "surface": "shared-renderer", + "snippet": "const metricsApi = window.VCPDesktop && window.VCPDesktop.metrics;" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 27, + "surface": "shared-renderer", + "snippet": "const metricsApi = window.VCPDesktop && window.VCPDesktop.metrics;" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 98, + "surface": "shared-renderer", + "snippet": "' var metricsApi = window.VCPDesktop && window.VCPDesktop.metrics;'," + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 98, + "surface": "shared-renderer", + "snippet": "' var metricsApi = window.VCPDesktop && window.VCPDesktop.metrics;'," + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 441, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop[item.exportKey] = {" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 447, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinSystemMonitor = {" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 457, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.metricWidgets = {" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 462, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.agentAPI = window.VCPDesktop.agentAPI || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 462, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.agentAPI = window.VCPDesktop.agentAPI || {};" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 463, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.agentAPI.metrics = {" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 465, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics && window.VCPDesktop.metrics.listComponents" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 465, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics && window.VCPDesktop.metrics.listComponents" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 466, + "surface": "shared-renderer", + "snippet": "? window.VCPDesktop.metrics.listComponents()" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 470, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics.getCapabilities(forceRefresh);" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 473, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics.getSnapshot(options);" + }, + { + "file": "Desktopmodules/builtinWidgets/systemMonitorWidget.js", + "line": 476, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics.getComponentSnapshot(component, options);" + }, + { + "file": "Desktopmodules/builtinWidgets/translateWidget.js", + "line": 12, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/translateWidget.js", + "line": 314, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinTranslate = {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 19, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 422, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 423, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('streaming', `正在启动: ${appDef.name}...`);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 424, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 442, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 443, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `已启动: ${appDef.name}`);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 444, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2000);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 448, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 449, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', `启动失败: ${result?.error || '未知错误'}`);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 450, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 456, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 457, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', `启动出错: ${err.message}`);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 458, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 527, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.render) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 527, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.render) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 528, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock.render();" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 531, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.saveDockConfig) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 531, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.saveDockConfig) {" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 532, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock.saveDockConfig();" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 600, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock?.render?.();" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 601, + "surface": "shared-renderer", + "snippet": "await window.VCPDesktop.dock?.saveDockConfig?.();" + }, + { + "file": "Desktopmodules/builtinWidgets/vchatApps.js", + "line": 635, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.vchatApps = {" + }, + { + "file": "Desktopmodules/builtinWidgets/weatherWidget.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/builtinWidgets/weatherWidget.js", + "line": 184, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinWeather = {" + }, + { + "file": "Desktopmodules/core/dragSystem.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { CONSTANTS, zIndex } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/dragSystem.js", + "line": 26, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.state && window.VCPDesktop.state.desktopLocked) return;" + }, + { + "file": "Desktopmodules/core/dragSystem.js", + "line": 26, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.state && window.VCPDesktop.state.desktopLocked) return;" + }, + { + "file": "Desktopmodules/core/dragSystem.js", + "line": 98, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.drag = {" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 95, + "surface": "shared-renderer", + "snippet": "const activeWidgetIds = Array.from(window.VCPDesktop.state.widgets.keys());" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 118, + "surface": "shared-renderer", + "snippet": "if (!window.VCPDesktop.state.widgets.has(id)) {" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 125, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.wallpaper) {" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 126, + "surface": "shared-renderer", + "snippet": "const config = window.VCPDesktop.wallpaper.getConfig();" + }, + { + "file": "Desktopmodules/core/performanceManager.js", + "line": 148, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.performanceManager = new PerformanceManager();" + }, + { + "file": "Desktopmodules/core/state.js", + "line": 102, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.state = desktopState;" + }, + { + "file": "Desktopmodules/core/state.js", + "line": 103, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.CONSTANTS = CONSTANTS;" + }, + { + "file": "Desktopmodules/core/state.js", + "line": 104, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.domRefs = domRefs;" + }, + { + "file": "Desktopmodules/core/state.js", + "line": 105, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.initDomRefs = initDomRefs;" + }, + { + "file": "Desktopmodules/core/statusIndicator.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { domRefs } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/statusIndicator.js", + "line": 56, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status = {" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 14, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 391, + "surface": "shared-renderer", + "snippet": "const metrics = window.VCPDesktop?.metrics;" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 460, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop?.globalSettings?.save) {" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 461, + "surface": "shared-renderer", + "snippet": "await window.VCPDesktop.globalSettings.save();" + }, + { + "file": "Desktopmodules/core/styleAutomation.js", + "line": 511, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.styleAutomation = {" + }, + { + "file": "Desktopmodules/core/theme.js", + "line": 56, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.theme = {" + }, + { + "file": "Desktopmodules/core/visibilityFreezer.js", + "line": 27, + "surface": "shared-renderer", + "snippet": "const { state, domRefs } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/visibilityFreezer.js", + "line": 393, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.visibilityFreezer = {" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 27, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 165, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 166, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '视频壁纸加载失败');" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 167, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 168, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 215, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 216, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', 'HTML壁纸加载失败');" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 217, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 218, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/core/wallpaperManager.js", + "line": 347, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.wallpaper = {" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, CONSTANTS, domRefs, drag, zIndex } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 110, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.contextMenu) {" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 111, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.contextMenu.show(e.clientX, e.clientY, widgetId);" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 569, + "surface": "shared-renderer", + "snippet": "var _wd = window.VCPDesktop.state.widgets.get('${widgetId}');" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 584, + "surface": "shared-renderer", + "snippet": "var _wd = window.VCPDesktop.state.widgets.get('${widgetId}');" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 596, + "surface": "shared-renderer", + "snippet": "var _wd = window.VCPDesktop.state.widgets.get('${widgetId}');" + }, + { + "file": "Desktopmodules/core/widgetManager.js", + "line": 728, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.widget = {" + }, + { + "file": "Desktopmodules/core/zIndexManager.js", + "line": 14, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/core/zIndexManager.js", + "line": 147, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.zIndex = {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const { state, widget } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 28, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinWeather) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 29, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinWeather.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 33, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinMusic) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 34, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinMusic.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 38, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinNews) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 39, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinNews.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 43, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinTranslate) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 44, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinTranslate.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 48, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.metricWidgets) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 49, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metricWidgets.spawn(component, options);" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 53, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinCpuMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 54, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinCpuMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 58, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinMemoryMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 59, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinMemoryMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 63, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinDiskMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 64, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinDiskMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 68, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinNetworkMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 69, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinNetworkMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 73, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinGpuMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 74, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinGpuMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 78, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinBatteryMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 79, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinBatteryMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 83, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinDockerMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 84, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinDockerMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 88, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinSensorsMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 89, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinSensorsMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 93, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinProcessMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 94, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinProcessMonitor.spawn();" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 98, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.builtinSystemMonitor) {" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 99, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.builtinSystemMonitor.spawn('cpu');" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 103, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics?.listComponents?.() || [];" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 106, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.metrics?.getComponentSnapshot?.(component, options);" + }, + { + "file": "Desktopmodules/debug/debugTools.js", + "line": 149, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.debug = {" + }, + { + "file": "Desktopmodules/desktop.js", + "line": 33, + "surface": "shared-renderer", + "snippet": "const D = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/desktop.js", + "line": 127, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.ipc.init();" + }, + { + "file": "Desktopmodules/desktop.js", + "line": 133, + "surface": "shared-renderer", + "snippet": "const D = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/favorites/favoritesManager.js", + "line": 10, + "surface": "shared-renderer", + "snippet": "const { state, status, widget, sidebar, thumbnail } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/favorites/favoritesManager.js", + "line": 203, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites = {" + }, + { + "file": "Desktopmodules/favorites/thumbnail.js", + "line": 118, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.thumbnail = {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 10, + "surface": "shared-renderer", + "snippet": "const { state, widget, zIndex } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 28, + "surface": "shared-renderer", + "snippet": "if (targetId && window.VCPDesktop.saveModal) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 29, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.saveModal.show(targetId);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 37, + "surface": "shared-renderer", + "snippet": "if (targetId && window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 38, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.refresh(targetId);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 166, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.sidebar) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 167, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar.toggle(true);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 168, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar.switchTab('presets');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 275, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 276, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '找不到原预设,请手动保存');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 277, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 278, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 313, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 314, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `预设已更新: ${target.name}`);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 315, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 316, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 320, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 321, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '保存预设失败');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 322, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 323, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 331, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 332, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '请先收藏该挂件,再编辑源码');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 333, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 334, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 341, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 342, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '代码编辑 API 不可用,请重启应用');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 343, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 344, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 356, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 357, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `正在编辑源码: ${widgetData.savedName || widgetData.savedId}`);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 358, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 359, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 361, + "surface": "shared-renderer", + "snippet": "} else if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 362, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', result?.error || '打开代码编辑失败');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 363, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 364, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 368, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 369, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '打开代码编辑失败');" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 370, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 371, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 479, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 482, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `${icon} ${msg}`);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 483, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 484, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2000);" + }, + { + "file": "Desktopmodules/ui/contextMenu.js", + "line": 494, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.contextMenu = {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 20, + "surface": "shared-renderer", + "snippet": "const { state, domRefs, CONSTANTS } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 645, + "surface": "shared-renderer", + "snippet": "if (item.builtinId && window.VCPDesktop[item.builtinId]) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 646, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop[item.builtinId].spawn();" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 653, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.vchatApps && window.VCPDesktop.vchatApps.launch) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 653, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.vchatApps && window.VCPDesktop.vchatApps.launch) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 654, + "surface": "shared-renderer", + "snippet": "await window.VCPDesktop.vchatApps.launch(item);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 667, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 668, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', `启动失败: ${result.error}`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 784, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 785, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('streaming', '正在扫描桌面快捷方式...');" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 786, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 793, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 794, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `已导入 ${count} 个快捷方式`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 795, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 799, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 800, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', `扫描失败: ${result?.error || '未知错误'}`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 801, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 806, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 807, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', '扫描失败');" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 808, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 919, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 920, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `已导入 ${count} 个快捷方式`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 921, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 922, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1226, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.iconPicker) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1227, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.iconPicker.open((iconData) => {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1349, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.iconPicker) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1350, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.iconPicker.open((iconData) => {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1482, + "surface": "shared-renderer", + "snippet": "const vApps = window.VCPDesktop.vchatApps;" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1562, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1563, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `已恢复默认图标: ${item.name}`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1564, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1565, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1568, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1569, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('waiting', `无法获取默认图标: ${item.name}`);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1570, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1571, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2000);" + }, + { + "file": "Desktopmodules/ui/dock.js", + "line": 1926, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock = {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 18, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 264, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 265, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', '设置已保存');" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 266, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 267, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2500);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 284, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.wallpaper) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 285, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.wallpaper.apply(state.globalSettings.wallpaper);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 288, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 289, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', '已恢复默认设置');" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 290, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 291, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 2500);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 352, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.visibilityFreezer) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 353, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.visibilityFreezer.setEnabled(s.visibilityFreezerEnabled !== false);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 359, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 360, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock.render();" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 370, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.applyPosition) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 370, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.dock && window.VCPDesktop.dock.applyPosition) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 378, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.dock.applyPosition(pos, dist);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 387, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.wallpaper) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 388, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.wallpaper.apply(s.wallpaper);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 406, + "surface": "shared-renderer", + "snippet": "if (defaultPreset && window.VCPDesktop.sidebar) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 411, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.sidebar.applyPreset) {" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 412, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar.applyPreset(defaultPreset);" + }, + { + "file": "Desktopmodules/ui/globalSettings.js", + "line": 835, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.globalSettings = {" + }, + { + "file": "Desktopmodules/ui/iconPicker.js", + "line": 313, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.iconPicker = {" + }, + { + "file": "Desktopmodules/ui/saveModal.js", + "line": 35, + "surface": "shared-renderer", + "snippet": "if (widgetId && window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/saveModal.js", + "line": 38, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.performSave(widgetId, name);" + }, + { + "file": "Desktopmodules/ui/saveModal.js", + "line": 70, + "surface": "shared-renderer", + "snippet": "const { state } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/saveModal.js", + "line": 88, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.saveModal = {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 10, + "surface": "shared-renderer", + "snippet": "const { state, domRefs } = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 61, + "surface": "shared-renderer", + "snippet": "if (tabName === 'favorites' && window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 62, + "surface": "shared-renderer", + "snippet": "return window.VCPDesktop.favorites.loadList();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 272, + "surface": "shared-renderer", + "snippet": "const D = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 336, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 337, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.spawnFromFavorite(fav.id);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 349, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 350, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.deleteFavorite(fav.id);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 426, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 427, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `布局预设已保存: ${name}`);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 428, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 429, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 477, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 478, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `空白预设已创建: ${name}`);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 479, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 480, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 622, + "surface": "shared-renderer", + "snippet": "const D = window.VCPDesktop;" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 722, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.globalSettings) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 723, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.globalSettings.save();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 862, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 863, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', `预设已重命名: ${newName}`);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 864, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 865, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 885, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 886, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', '已取消默认预设');" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 887, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 888, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 892, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.status) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 893, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.update('connected', '已设为默认预设(下次启动自动加载)');" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 894, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.status.show();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 895, + "surface": "shared-renderer", + "snippet": "setTimeout(() => window.VCPDesktop.status.hide(), 3000);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 900, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.globalSettings) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 901, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.globalSettings.save();" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 1087, + "surface": "shared-renderer", + "snippet": "if (window.VCPDesktop.favorites) {" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 1088, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.favorites.spawnFromFavorite(favId, e.clientX - 100, e.clientY - 30);" + }, + { + "file": "Desktopmodules/ui/sidebar.js", + "line": 1107, + "surface": "shared-renderer", + "snippet": "window.VCPDesktop.sidebar = {" + } + ], + "tests": [] + }, + "VCPFrontendPlugins": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 23, + "syntax": "dot-assignment", + "snippet": "window.VCPFrontendPlugins = api;" + } + ], + "production": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js", + "line": 20, + "surface": "shared-renderer", + "snippet": "if (window.VCPFrontendPlugins?.get(ID)) return;" + }, + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js", + "line": 730, + "surface": "shared-renderer", + "snippet": "const hunbao = window.VCPFrontendPlugins?.get('vchat-dynamic-wallpaper');" + }, + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js", + "line": 802, + "surface": "shared-renderer", + "snippet": "window.VCPFrontendPlugins?.register(ID, {" + }, + { + "file": "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "line": 5, + "surface": "shared-renderer", + "snippet": "if (window.VCPFrontendPlugins?.get(ID)) return;" + }, + { + "file": "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "line": 218, + "surface": "shared-renderer", + "snippet": "window.VCPFrontendPlugins?.register(ID, { destroy, settings: () => settings, syncControls });" + }, + { + "file": "VCPDistributedServer/Plugin/VChatDynamicWallpaper/plugin.js", + "line": 5, + "surface": "shared-renderer", + "snippet": "if (window.VCPFrontendPlugins?.get(ID)) return;" + }, + { + "file": "VCPDistributedServer/Plugin/VChatDynamicWallpaper/plugin.js", + "line": 443, + "surface": "shared-renderer", + "snippet": "window.VCPFrontendPlugins?.register(ID, { destroy, selectFolder, setPlaylist, state, applyVisibility });" + }, + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 4, + "surface": "shared-renderer", + "snippet": "if (window.VCPFrontendPlugins?.loaderStarted) return;" + } + ], + "tests": [] + }, + "VCPIcons": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/lucide-adapter.js", + "line": 848, + "syntax": "dot-assignment", + "snippet": "window.VCPIcons = Object.freeze({ refresh, set });" + } + ], + "production": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 127, + "surface": "internal-app", + "snippet": "if (window.VCPIcons?.set) window.VCPIcons.set(icon, state.maximized ? 'filter_none' : 'crop_square');" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 127, + "surface": "internal-app", + "snippet": "if (window.VCPIcons?.set) window.VCPIcons.set(icon, state.maximized ? 'filter_none' : 'crop_square');" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 774, + "surface": "internal-app", + "snippet": "setIcon: (element, icon) => window.VCPIcons?.set?.(element, icon)," + }, + { + "file": "modules/ui-system/vcp-ui.js", + "line": 28, + "surface": "internal-app", + "snippet": "if (span.isConnected) window.VCPIcons?.set?.(span, name);" + } + ], + "tests": [] + }, + "VCPMainChatState": { + "classification": "supported-public-facade", + "definitions": [ + { + "file": "renderer.js", + "line": 106, + "syntax": "define-property", + "snippet": "Object.defineProperty(window, 'VCPMainChatState', {" + } + ], + "production": [ + { + "file": "Flowlockmodules/flowlock-integration.js", + "line": 9, + "surface": "flowlock", + "snippet": "const mainChatSnapshot = () => window.VCPMainChatState?.snapshot?.() || { selectedItem: null, topicId: null };" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 118, + "surface": "flowlock", + "snippet": "const resolvedAgentId = agentId || window.VCPMainChatState?.snapshot?.()?.selectedItem?.id;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 638, + "surface": "flowlock", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 654, + "surface": "flowlock", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "Flowlockmodules/flowlock.js", + "line": 670, + "surface": "flowlock", + "snippet": "const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + }, + { + "file": "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "line": 31, + "surface": "shared-renderer", + "snippet": "const item = window.VCPMainChatState?.snapshot?.()?.selectedItem;" + } + ], + "tests": [] + }, + "VCPNextShellController": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 941, + "syntax": "dot-assignment", + "snippet": "window.VCPNextShellController = Object.freeze({" + } + ], + "production": [ + { + "file": "modules/topTabManager.js", + "line": 3, + "surface": "shared-renderer", + "snippet": "const controller = () => window.VCPNextShellController;" + }, + { + "file": "renderer.js", + "line": 571, + "surface": "main-window", + "snippet": "provideCapabilities: window.VCPNextShellController?.provideChatCapabilities," + } + ], + "tests": [ + { + "file": "tests/lifecycle-inspector.test.js", + "line": 13, + "surface": "test", + "snippet": "window.VCPNextShellController = { getDiagnostics: () => ({ mounted: true, openViews: ['app:safe'] }) };" + } + ] + }, + "VCPUI": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2383, + "syntax": "dot-assignment", + "snippet": "window.VCPUI = VCPUI;" + } + ], + "production": [ + { + "file": "modules/ui-helpers.js", + "line": 379, + "surface": "shared-renderer", + "snippet": "if (window.VCPUI?.feedback?.toast) {" + }, + { + "file": "modules/ui-helpers.js", + "line": 381, + "surface": "shared-renderer", + "snippet": "return window.VCPUI.feedback.toast(String(message), { variant, duration });" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 466, + "surface": "internal-app", + "snippet": "window.VCPUI?.getController?.(avatarControl)?.update({" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 480, + "surface": "internal-app", + "snippet": "window.VCPUI?.getController?.(customRadiusControl)?.update({ value: customRadius });" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 744, + "surface": "internal-app", + "snippet": "if (window.VCPUI?.getController?.(control)) return;" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 745, + "surface": "internal-app", + "snippet": "const controller = window.VCPUI?.enhance?.('Range', control);" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 946, + "surface": "internal-app", + "snippet": "window.VCPUI?.getController?.(button)?.update({ value: current });" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 211, + "surface": "internal-app", + "snippet": "const VCPUI = options.VCPUI || window.VCPUI;" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 587, + "surface": "internal-app", + "snippet": "window.VCPUI?.feedback?.toast?.('复制失败', { variant: 'error' });" + }, + { + "file": "modules/ui-system/ask-nova-modal.js", + "line": 635, + "surface": "internal-app", + "snippet": "window.VCPUI?.feedback?.toast?.(error?.message || 'Ask Nova 打开失败', { variant: 'error' });" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 227, + "surface": "internal-app", + "snippet": "feedback: window.VCPUI?.feedback," + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 406, + "surface": "internal-app", + "snippet": "window.VCPUI?.feedback?.toast?.(error.message || '标签拖出失败', { variant: 'error' });" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 527, + "surface": "internal-app", + "snippet": "window.VCPUI?.feedback.cancelAll();" + }, + { + "file": "modules/ui-system/next-shell/next-shell-controller.js", + "line": 809, + "surface": "internal-app", + "snippet": "getUi: () => window.VCPUI," + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 57, + "surface": "internal-app", + "snippet": "if (!element || window.VCPUI.getController(element)) return;" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 59, + "surface": "internal-app", + "snippet": "const controller = window.VCPUI.enhance(name, element, options);" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 229, + "surface": "internal-app", + "snippet": "state.list = window.VCPUI.create('List', { items });" + } + ], + "tests": [] + }, + "VCPUISettingsBridge": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/settings-bridge.js", + "line": 456, + "syntax": "dot-assignment", + "snippet": "window.VCPUISettingsBridge = Object.freeze({" + } + ], + "production": [ + { + "file": "modules/ui-system/vcp-main-ui-runtime.js", + "line": 38, + "surface": "internal-app", + "snippet": "window.VCPUISettingsBridge?.refresh?.();" + } + ], + "tests": [] + }, + "VCPWebAwesome": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 231, + "syntax": "dot-assignment", + "snippet": "window.VCPWebAwesome = Object.freeze({" + } + ], + "production": [ + { + "file": "modules/ui-system/settings-bridge.js", + "line": 114, + "surface": "internal-app", + "snippet": "if (window.VCPWebAwesome?.isLoaded?.('select')) {" + }, + { + "file": "modules/ui-system/vcp-main-ui-runtime.js", + "line": 22, + "surface": "internal-app", + "snippet": "await window.VCPWebAwesome?.loadComponents?.();" + }, + { + "file": "modules/ui-system/vcp-main-ui-runtime.js", + "line": 25, + "surface": "internal-app", + "snippet": "const mountedRelease = window.VCPWebAwesome?.mountScope?.(document.body) || null;" + }, + { + "file": "modules/ui-system/vcp-ui.js", + "line": 740, + "surface": "internal-app", + "snippet": "if (!window.VCPWebAwesome?.isDefined?.(tag)) return null;" + }, + { + "file": "modules/ui-system/vcp-ui.js", + "line": 741, + "surface": "internal-app", + "snippet": "return window.VCPWebAwesome.create(tag, attrs);" + }, + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2348, + "surface": "internal-app", + "snippet": "&& window.VCPWebAwesome?.isDefined?.('select');" + } + ], + "tests": [] + }, + "VDocContainer": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/vdoc-container.js", + "line": 301, + "syntax": "dot-assignment", + "snippet": "window.VDocContainer = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 6, + "surface": "shared-renderer", + "snippet": "const containerModule = window.VDocContainer;" + } + ], + "tests": [] + }, + "VDocCore": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/vdoc-core.js", + "line": 657, + "syntax": "dot-assignment", + "snippet": "window.VDocCore = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 5, + "surface": "shared-renderer", + "snippet": "const core = window.VDocCore;" + }, + { + "file": "ScriptoriumModules/vdoc-style-library.js", + "line": 185, + "surface": "shared-renderer", + "snippet": "const sanitizer = window.VDocCore?.sanitizeCss;" + } + ], + "tests": [] + }, + "VDocPagination": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/scriptorium-pagination.js", + "line": 693, + "syntax": "dot-assignment", + "snippet": "window.VDocPagination = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const pagination = window.VDocPagination;" + } + ], + "tests": [] + }, + "VDocStyleLibrary": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/vdoc-style-library.js", + "line": 545, + "syntax": "dot-assignment", + "snippet": "window.VDocStyleLibrary = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 8, + "surface": "shared-renderer", + "snippet": "const styleLibrary = window.VDocStyleLibrary;" + } + ], + "tests": [] + }, + "VDocSvgAssetLibrary": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "ScriptoriumModules/vdoc-svg-asset-library.js", + "line": 458, + "syntax": "dot-assignment", + "snippet": "window.VDocSvgAssetLibrary = Object.freeze({" + } + ], + "production": [ + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 27, + "surface": "shared-renderer", + "snippet": "svgAssetLibrary: window.VDocSvgAssetLibrary," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 821, + "surface": "shared-renderer", + "snippet": "svgAssetLibrary: window.VDocSvgAssetLibrary," + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 837, + "surface": "shared-renderer", + "snippet": "window.VDocSvgAssetLibrary.exportUserPacks()" + }, + { + "file": "ScriptoriumModules/scriptorium.js", + "line": 862, + "surface": "shared-renderer", + "snippet": "library: window.VDocSvgAssetLibrary," + } + ], + "tests": [] + }, + "wallpaperPluginListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 94, + "syntax": "dot-assignment", + "snippet": "window.wallpaperPluginListener = window.wallpaperPluginListener || {};" + } + ], + "production": [], + "tests": [] + }, + "wallpaperPropertyListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 64, + "syntax": "define-property", + "snippet": "Object.defineProperty(window, 'wallpaperPropertyListener', {" + }, + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 77, + "syntax": "dot-assignment", + "snippet": "window.wallpaperPropertyListener = null;" + } + ], + "production": [], + "tests": [] + }, + "wallpaperRegisterAudioListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 82, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRegisterAudioListener = function (cb) {" + } + ], + "production": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 97, + "surface": "shared-renderer", + "snippet": "window.wallpaperRegisterAudioListener.isVCPShim = true;" + } + ], + "tests": [] + }, + "wallpaperRegisterMediaPlaybackListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 92, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRegisterMediaPlaybackListener = NOOP;" + } + ], + "production": [], + "tests": [] + }, + "wallpaperRegisterMediaPropertiesListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 89, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRegisterMediaPropertiesListener = NOOP;" + } + ], + "production": [], + "tests": [] + }, + "wallpaperRegisterMediaThumbnailListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 90, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRegisterMediaThumbnailListener = NOOP;" + } + ], + "production": [], + "tests": [] + }, + "wallpaperRegisterMediaTimelineListener": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 91, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRegisterMediaTimelineListener = NOOP;" + } + ], + "production": [], + "tests": [] + }, + "wallpaperRequestRandomFileForProperty": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "line": 93, + "syntax": "dot-assignment", + "snippet": "window.wallpaperRequestRandomFileForProperty = NOOP;" + } + ], + "production": [], + "tests": [] + }, + "weatherService": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "modules/weatherService.js", + "line": 403, + "syntax": "dot-assignment", + "snippet": "window.weatherService = weatherService;" + } + ], + "production": [ + { + "file": "renderer.js", + "line": 258, + "surface": "main-window", + "snippet": "window.weatherService?.configureCapabilities?.({ settings: mainChatSettingsOwner });" + } + ], + "tests": [] + }, + "workflowEditor": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 166, + "syntax": "dot-assignment", + "snippet": "window.workflowEditor = window.WorkflowEditor_Config.getInstance();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 418, + "syntax": "dot-assignment", + "snippet": "window.workflowEditor = WorkflowEditor_Config.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 143, + "surface": "shared-renderer", + "snippet": "workflowEditor: !!window.workflowEditor" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 156, + "surface": "shared-renderer", + "snippet": "window.workflowEditor) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 162, + "surface": "shared-renderer", + "snippet": "const initPromise = window.workflowEditor.init();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 199, + "surface": "shared-renderer", + "snippet": "workflowEditor: !!window.workflowEditor" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 218, + "surface": "shared-renderer", + "snippet": "delete window.workflowEditor;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 167, + "surface": "shared-renderer", + "snippet": "console.log('[WorkflowEditorLoader] ✓ 设置 window.workflowEditor 引用:', window.workflowEditor);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 167, + "surface": "shared-renderer", + "snippet": "console.log('[WorkflowEditorLoader] ✓ 设置 window.workflowEditor 引用:', window.workflowEditor);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 168, + "surface": "shared-renderer", + "snippet": "console.log('[WorkflowEditorLoader] ✓ workflowEditor.show 方法存在:', typeof window.workflowEditor.show);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 169, + "surface": "shared-renderer", + "snippet": "console.log('[WorkflowEditorLoader] ✓ workflowEditor.init 方法存在:', typeof window.workflowEditor.init);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 437, + "surface": "shared-renderer", + "snippet": "if (window.workflowEditor) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 438, + "surface": "shared-renderer", + "snippet": "const nodeId = window.workflowEditor.addNode(nodeData);" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1685, + "surface": "shared-renderer", + "snippet": "if (window.workflowEditor) {" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1686, + "surface": "shared-renderer", + "snippet": "window.workflowEditor.show();" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1709, + "surface": "shared-renderer", + "snippet": "if (window.workflowEditor) {" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1710, + "surface": "shared-renderer", + "snippet": "await window.workflowEditor.init();" + } + ], + "tests": [] + }, + "WorkflowEditor_ApiConfigDialog": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ApiConfigDialog.js", + "line": 466, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_ApiConfigDialog = new WorkflowEditor_ApiConfigDialog();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "line": 30, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ApiConfigDialog) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "line": 31, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_ApiConfigDialog.init(this);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "line": 803, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ApiConfigDialog) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "line": 804, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_ApiConfigDialog.show();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 15, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ApiConfigDialog && window.WorkflowEditor_ApiConfigDialog.getCurrentConfig) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 15, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ApiConfigDialog && window.WorkflowEditor_ApiConfigDialog.getCurrentConfig) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 16, + "surface": "shared-renderer", + "snippet": "return window.WorkflowEditor_ApiConfigDialog.getCurrentConfig();" + } + ], + "tests": [] + }, + "WorkflowEditor_CanvasManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 154, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_CanvasManager = this.components.canvasManager;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 140, + "surface": "shared-renderer", + "snippet": "canvasManager: !!window.WorkflowEditor_CanvasManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 153, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_CanvasManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 196, + "surface": "shared-renderer", + "snippet": "canvasManager: !!window.WorkflowEditor_CanvasManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 216, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 52, + "surface": "shared-renderer", + "snippet": "this.canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1671, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_CanvasManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1672, + "surface": "shared-renderer", + "snippet": "canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js", + "line": 560, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_CanvasManager && window.WorkflowEditor_CanvasManager.updateNodeInputs) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js", + "line": 560, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_CanvasManager && window.WorkflowEditor_CanvasManager.updateNodeInputs) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js", + "line": 561, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_CanvasManager.updateNodeInputs(id, nodeData.dynamicInputs);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 363, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 1291, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2092, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2113, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2202, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2218, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2268, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2305, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2351, + "surface": "shared-renderer", + "snippet": "const canvasManager = window.WorkflowEditor_CanvasManager;" + } + ], + "tests": [] + }, + "WorkflowEditor_Config": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 417, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_Config = WorkflowEditor_Config;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 165, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_Config) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 166, + "surface": "shared-renderer", + "snippet": "window.workflowEditor = window.WorkflowEditor_Config.getInstance();" + } + ], + "tests": [] + }, + "WorkflowEditor_ConnectionManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager.js", + "line": 524, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_ConnectionManager = WorkflowEditor_ConnectionManager;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 136, + "surface": "shared-renderer", + "snippet": "connectionManager: !!window.WorkflowEditor_ConnectionManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 149, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_ConnectionManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 192, + "surface": "shared-renderer", + "snippet": "connectionManager: !!window.WorkflowEditor_ConnectionManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 76, + "surface": "shared-renderer", + "snippet": "} else if (window.WorkflowEditor_ConnectionManager) {" + } + ], + "tests": [] + }, + "WorkflowEditor_ConnectionManager_Instance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 159, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_ConnectionManager_Instance = this.components.connectionManager;" + } + ], + "production": [], + "tests": [] + }, + "WorkflowEditor_ConnectionManager_Simplified": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager_Simplified.js", + "line": 215, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_ConnectionManager_Simplified = WorkflowEditor_ConnectionManager_Simplified;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 74, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ConnectionManager_Simplified) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 105, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ConnectionManager_Simplified) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 106, + "surface": "shared-renderer", + "snippet": "this.components.connectionManager = new window.WorkflowEditor_ConnectionManager_Simplified();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 34, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_ConnectionManager_Simplified) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 35, + "surface": "shared-renderer", + "snippet": "this.connectionManager = new window.WorkflowEditor_ConnectionManager_Simplified();" + } + ], + "tests": [] + }, + "WorkflowEditor_ExecutionEngine": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 2773, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_ExecutionEngine = WorkflowEditor_ExecutionEngine.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 142, + "surface": "shared-renderer", + "snippet": "executionEngine: !!window.WorkflowEditor_ExecutionEngine," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 155, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_ExecutionEngine &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 198, + "surface": "shared-renderer", + "snippet": "executionEngine: !!window.WorkflowEditor_ExecutionEngine," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 101, + "surface": "shared-renderer", + "snippet": "this.components.executionEngine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 40, + "surface": "shared-renderer", + "snippet": "this.executionEngine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 790, + "surface": "shared-renderer", + "snippet": "const engine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 800, + "surface": "shared-renderer", + "snippet": "const engine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1825, + "surface": "shared-renderer", + "snippet": "const engine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 1298, + "surface": "shared-renderer", + "snippet": "const executionEngine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2482, + "surface": "shared-renderer", + "snippet": "const executionEngine = window.WorkflowEditor_ExecutionEngine;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2511, + "surface": "shared-renderer", + "snippet": "const executionEngine = window.WorkflowEditor_ExecutionEngine;" + } + ], + "tests": [] + }, + "WorkflowEditor_NodeManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 2013, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_NodeManager = WorkflowEditor_NodeManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 141, + "surface": "shared-renderer", + "snippet": "nodeManager: !!window.WorkflowEditor_NodeManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 154, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_NodeManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 197, + "surface": "shared-renderer", + "snippet": "nodeManager: !!window.WorkflowEditor_NodeManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 217, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_NodeManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 93, + "surface": "shared-renderer", + "snippet": "this.components.nodeManager = window.WorkflowEditor_NodeManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 56, + "surface": "shared-renderer", + "snippet": "this.nodeManager = window.WorkflowEditor_NodeManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 567, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeAiComposeNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 567, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeAiComposeNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 569, + "surface": "shared-renderer", + "snippet": "const result = await window.WorkflowEditor_NodeManager.executeAiComposeNode(node, inputData);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1274, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeUrlExtractorNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1274, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeUrlExtractorNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1276, + "surface": "shared-renderer", + "snippet": "const result = await window.WorkflowEditor_NodeManager.executeUrlExtractorNode(node, inputData);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1295, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeImageUploadNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1295, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager.executeImageUploadNode) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1297, + "surface": "shared-renderer", + "snippet": "const result = await window.WorkflowEditor_NodeManager.executeImageUploadNode(node, inputData);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1424, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && typeof window.WorkflowEditor_NodeManager.executeUrlRendererNode === 'function') {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1424, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && typeof window.WorkflowEditor_NodeManager.executeUrlRendererNode === 'function') {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1426, + "surface": "shared-renderer", + "snippet": "return window.WorkflowEditor_NodeManager.executeUrlRendererNode.call(" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js", + "line": 1427, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_NodeManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1387, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && typeof window.WorkflowEditor_NodeManager.ensureUrlRendererEnhancements === 'function') {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1387, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && typeof window.WorkflowEditor_NodeManager.ensureUrlRendererEnhancements === 'function') {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1388, + "surface": "shared-renderer", + "snippet": "try { window.WorkflowEditor_NodeManager.ensureUrlRendererEnhancements(); } catch (e) { }" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor.js", + "line": 8, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const nodeManager = window.WorkflowEditor_NodeManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 8, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 9, + "surface": "shared-renderer", + "snippet": "const nodeManager = window.WorkflowEditor_NodeManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 181, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager !== nodeManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 181, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_NodeManager && window.WorkflowEditor_NodeManager !== nodeManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 182, + "surface": "shared-renderer", + "snippet": "typeof window.WorkflowEditor_NodeManager.executeUrlRendererNode === 'function') {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js", + "line": 183, + "surface": "shared-renderer", + "snippet": "return window.WorkflowEditor_NodeManager.executeUrlRendererNode(node, inputData);" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 31, + "surface": "shared-renderer", + "snippet": "this.nodeManager = window.WorkflowEditor_NodeManager || null;" + } + ], + "tests": [] + }, + "WorkflowEditor_NodeManager_Instance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 155, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_NodeManager_Instance = this.components.nodeManager;" + } + ], + "production": [], + "tests": [] + }, + "WorkflowEditor_PluginDialog": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 585, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_PluginDialog = WorkflowEditor_PluginDialog.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 138, + "surface": "shared-renderer", + "snippet": "pluginDialog: !!window.WorkflowEditor_PluginDialog," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 151, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_PluginDialog &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 194, + "surface": "shared-renderer", + "snippet": "pluginDialog: !!window.WorkflowEditor_PluginDialog," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 214, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_PluginDialog;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 44, + "surface": "shared-renderer", + "snippet": "this.pluginDialog = window.WorkflowEditor_PluginDialog;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 43, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 57, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 143, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 144, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 152, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 153, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 154, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 290, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 291, + "surface": "shared-renderer", + "snippet": "${plugin.isCustom ? `` : ''}" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 344, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 345, + "surface": "shared-renderer", + "snippet": "${plugin.isCustom ? `` : ''}" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 577, + "surface": "shared-renderer", + "snippet": "" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2452, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginDialog) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2453, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_PluginDialog.show();" + } + ], + "tests": [] + }, + "WorkflowEditor_PluginManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js", + "line": 920, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_PluginManager = WorkflowEditor_PluginManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 137, + "surface": "shared-renderer", + "snippet": "pluginManager: !!window.WorkflowEditor_PluginManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 150, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_PluginManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 193, + "surface": "shared-renderer", + "snippet": "pluginManager: !!window.WorkflowEditor_PluginManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 213, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 97, + "surface": "shared-renderer", + "snippet": "this.components.pluginManager = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 36, + "surface": "shared-renderer", + "snippet": "this.pluginManager = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1636, + "surface": "shared-renderer", + "snippet": "const pluginManager = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js", + "line": 1748, + "surface": "shared-renderer", + "snippet": "const pluginManager = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 386, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 398, + "surface": "shared-renderer", + "snippet": "const pluginManager = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 521, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 806, + "surface": "shared-renderer", + "snippet": "const pm = window.WorkflowEditor_PluginManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2459, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2460, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_PluginManager.showApiConfigDialog();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 12, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginManager && window.WorkflowEditor_PluginManager.getApiConfig) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 12, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_PluginManager && window.WorkflowEditor_PluginManager.getApiConfig) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js", + "line": 13, + "surface": "shared-renderer", + "snippet": "return window.WorkflowEditor_PluginManager.getApiConfig();" + } + ], + "tests": [] + }, + "WorkflowEditor_PluginManager_Instance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 156, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_PluginManager_Instance = this.components.pluginManager;" + } + ], + "production": [], + "tests": [] + }, + "WorkflowEditor_StateManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js", + "line": 835, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_StateManager = WorkflowEditor_StateManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 135, + "surface": "shared-renderer", + "snippet": "stateManager: !!window.WorkflowEditor_StateManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 148, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_StateManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 191, + "surface": "shared-renderer", + "snippet": "stateManager: !!window.WorkflowEditor_StateManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 212, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_StateManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 85, + "surface": "shared-renderer", + "snippet": "this.components.stateManager = window.WorkflowEditor_StateManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 33, + "surface": "shared-renderer", + "snippet": "this.stateManager = window.WorkflowEditor_StateManager;" + } + ], + "tests": [] + }, + "WorkflowEditor_StateManager_Instance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 153, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_StateManager_Instance = this.components.stateManager;" + } + ], + "production": [], + "tests": [] + }, + "WorkflowEditor_UIManager": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js", + "line": 2562, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_UIManager = WorkflowEditor_UIManager.getInstance();" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 139, + "surface": "shared-renderer", + "snippet": "uiManager: !!window.WorkflowEditor_UIManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 152, + "surface": "shared-renderer", + "snippet": "window.WorkflowEditor_UIManager &&" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 195, + "surface": "shared-renderer", + "snippet": "uiManager: !!window.WorkflowEditor_UIManager," + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 215, + "surface": "shared-renderer", + "snippet": "delete window.WorkflowEditor_UIManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 111, + "surface": "shared-renderer", + "snippet": "this.components.uiManager = window.WorkflowEditor_UIManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js", + "line": 48, + "surface": "shared-renderer", + "snippet": "this.uiManager = window.WorkflowEditor_UIManager;" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 395, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditor_UIManager) {" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js", + "line": 396, + "surface": "shared-renderer", + "snippet": "await window.WorkflowEditor_UIManager.refreshPluginPanel();" + } + ], + "tests": [] + }, + "WorkflowEditor_UIManager_Instance": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 162, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditor_UIManager_Instance = this.components.uiManager;" + } + ], + "production": [], + "tests": [] + }, + "workflowEditorLoader": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 227, + "syntax": "dot-assignment", + "snippet": "window.workflowEditorLoader = loaderInstance;" + } + ], + "production": [], + "tests": [] + }, + "WorkflowEditorLoader": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js", + "line": 242, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditorLoader = new WorkflowEditorLoader();" + }, + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 230, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditorLoader = loaderInstance;" + } + ], + "production": [ + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1706, + "surface": "shared-renderer", + "snippet": "if (window.WorkflowEditorLoader) {" + }, + { + "file": "VCPHumanToolBox/renderer.js", + "line": 1707, + "surface": "shared-renderer", + "snippet": "await window.WorkflowEditorLoader.load();" + } + ], + "tests": [] + }, + "WorkflowEditorLoader_Simplified": { + "classification": "legacy-or-feature-local-ambient", + "definitions": [ + { + "file": "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js", + "line": 223, + "syntax": "dot-assignment", + "snippet": "window.WorkflowEditorLoader_Simplified = WorkflowEditorLoader_Simplified;" + } + ], + "production": [], + "tests": [] + } + }, + "providerFiles": { + "chatManager": "modules/chatManager.js", + "messageRenderer": "modules/messageRenderer.js", + "streamManager": "modules/renderer/streamManager.js" + }, + "kernelFiles": [ + "modules/chat/chatContext.js", + "modules/chat/chatRepository.js", + "modules/chat/contentRuntime.js", + "modules/chat/contentTransforms.js", + "modules/chat/chatDomRenderer.js", + "modules/chat/chatSurface.js", + "modules/chat/chatOperation.js", + "modules/chat/chatPresentationState.js", + "modules/chat/chatPresentationSkin.js", + "modules/chat/chatSurfaceSlots.js", + "modules/chat/chatThemePlugin.js", + "modules/chat/chatPluginManifest.js", + "modules/chat/streamSession.js", + "modules/chat/streamCoordinator.js", + "modules/chat/streamTransientHistory.js", + "modules/chat/streamConsumerRegistry.js", + "modules/chat/vcpStreamBridge.js" + ], + "pureKernelFiles": [ + "modules/chat/chatContext.js", + "modules/chat/contentRuntime.js", + "modules/chat/contentTransforms.js", + "modules/chat/chatDomRenderer.js", + "modules/chat/chatSurface.js", + "modules/chat/chatOperation.js", + "modules/chat/chatPresentationState.js", + "modules/chat/chatPresentationSkin.js", + "modules/chat/chatSurfaceSlots.js", + "modules/chat/chatThemePlugin.js", + "modules/chat/chatPluginManifest.js", + "modules/chat/streamSession.js", + "modules/chat/streamCoordinator.js", + "modules/chat/streamTransientHistory.js", + "modules/chat/streamConsumerRegistry.js", + "modules/chat/vcpStreamBridge.js" + ], + "ownershipSignals": { + "Agenttaskmodules/task.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Agenttaskmodules/task.js": { + "windowReferences": 20, + "documentReferences": 48, + "electronReferences": 1, + "timers": 6, + "listeners": 18, + "mutableMaps": 1 + }, + "AppData/LoomApps/example-mobile/inject.js": { + "windowReferences": 0, + "documentReferences": 37, + "electronReferences": 0, + "timers": 1, + "listeners": 1, + "mutableMaps": 0 + }, + "Canvasmodules/canvas.html": { + "windowReferences": 6, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Canvasmodules/canvas.js": { + "windowReferences": 8, + "documentReferences": 51, + "electronReferences": 1, + "timers": 2, + "listeners": 35, + "mutableMaps": 0 + }, + "Desktopmodules/api/desktopMetrics.js": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/api/ipcBridge.js": { + "windowReferences": 34, + "documentReferences": 1, + "electronReferences": 1, + "timers": 4, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/api/vcpProxy.js": { + "windowReferences": 7, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/appTrayWidget.js": { + "windowReferences": 7, + "documentReferences": 9, + "electronReferences": 0, + "timers": 2, + "listeners": 7, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/musicWidget.js": { + "windowReferences": 5, + "documentReferences": 8, + "electronReferences": 0, + "timers": 6, + "listeners": 4, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/newsWidget.js": { + "windowReferences": 9, + "documentReferences": 5, + "electronReferences": 1, + "timers": 3, + "listeners": 4, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/performanceMonitorWidget.js": { + "windowReferences": 6, + "documentReferences": 2, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/systemMonitorWidget.js": { + "windowReferences": 19, + "documentReferences": 8, + "electronReferences": 0, + "timers": 3, + "listeners": 1, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/translateWidget.js": { + "windowReferences": 4, + "documentReferences": 13, + "electronReferences": 0, + "timers": 3, + "listeners": 6, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/vchatApps.js": { + "windowReferences": 36, + "documentReferences": 0, + "electronReferences": 1, + "timers": 3, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/builtinWidgets/weatherWidget.js": { + "windowReferences": 5, + "documentReferences": 3, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/core/dragSystem.js": { + "windowReferences": 8, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 3, + "mutableMaps": 0 + }, + "Desktopmodules/core/performanceManager.js": { + "windowReferences": 9, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "Desktopmodules/core/state.js": { + "windowReferences": 6, + "documentReferences": 2, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "Desktopmodules/core/statusIndicator.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/core/styleAutomation.js": { + "windowReferences": 7, + "documentReferences": 6, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "Desktopmodules/core/theme.js": { + "windowReferences": 6, + "documentReferences": 6, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/core/visibilityFreezer.js": { + "windowReferences": 7, + "documentReferences": 9, + "electronReferences": 1, + "timers": 2, + "listeners": 1, + "mutableMaps": 0 + }, + "Desktopmodules/core/wallpaperManager.js": { + "windowReferences": 12, + "documentReferences": 7, + "electronReferences": 0, + "timers": 4, + "listeners": 5, + "mutableMaps": 0 + }, + "Desktopmodules/core/widgetManager.js": { + "windowReferences": 25, + "documentReferences": 15, + "electronReferences": 1, + "timers": 14, + "listeners": 7, + "mutableMaps": 0 + }, + "Desktopmodules/core/zIndexManager.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/debug/debugTools.js": { + "windowReferences": 38, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/desktop.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/desktop.js": { + "windowReferences": 8, + "documentReferences": 8, + "electronReferences": 1, + "timers": 3, + "listeners": 8, + "mutableMaps": 0 + }, + "Desktopmodules/favorites/favoritesManager.js": { + "windowReferences": 6, + "documentReferences": 0, + "electronReferences": 1, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/favorites/thumbnail.js": { + "windowReferences": 6, + "documentReferences": 1, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Desktopmodules/ui/contextMenu.js": { + "windowReferences": 54, + "documentReferences": 10, + "electronReferences": 1, + "timers": 11, + "listeners": 11, + "mutableMaps": 0 + }, + "Desktopmodules/ui/dock.js": { + "windowReferences": 50, + "documentReferences": 62, + "electronReferences": 1, + "timers": 14, + "listeners": 51, + "mutableMaps": 1 + }, + "Desktopmodules/ui/globalSettings.js": { + "windowReferences": 28, + "documentReferences": 78, + "electronReferences": 1, + "timers": 3, + "listeners": 18, + "mutableMaps": 0 + }, + "Desktopmodules/ui/iconPicker.js": { + "windowReferences": 5, + "documentReferences": 9, + "electronReferences": 1, + "timers": 1, + "listeners": 7, + "mutableMaps": 0 + }, + "Desktopmodules/ui/saveModal.js": { + "windowReferences": 6, + "documentReferences": 2, + "electronReferences": 0, + "timers": 3, + "listeners": 4, + "mutableMaps": 0 + }, + "Desktopmodules/ui/sidebar.js": { + "windowReferences": 42, + "documentReferences": 45, + "electronReferences": 1, + "timers": 11, + "listeners": 31, + "mutableMaps": 0 + }, + "Dicemodules/dice.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Dicemodules/dice.js": { + "windowReferences": 2, + "documentReferences": 4, + "electronReferences": 1, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "Flowlockmodules/flowlock-integration.js": { + "windowReferences": 35, + "documentReferences": 2, + "electronReferences": 5, + "timers": 0, + "listeners": 4, + "mutableMaps": 0 + }, + "Flowlockmodules/flowlock-protocol.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Flowlockmodules/flowlock.js": { + "windowReferences": 7, + "documentReferences": 6, + "electronReferences": 9, + "timers": 3, + "listeners": 0, + "mutableMaps": 2 + }, + "Forummodules/forum.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Forummodules/forum.js": { + "windowReferences": 14, + "documentReferences": 71, + "electronReferences": 1, + "timers": 6, + "listeners": 39, + "mutableMaps": 4 + }, + "Groupmodules/groupSettingsMarkup.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Groupmodules/groupchat.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 1 + }, + "Groupmodules/grouprenderer.js": { + "windowReferences": 5, + "documentReferences": 48, + "electronReferences": 23, + "timers": 0, + "listeners": 11, + "mutableMaps": 1 + }, + "Groupmodules/modes/baseChatMode.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Groupmodules/modes/inviteOnlyMode.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Groupmodules/modes/natureRandomMode.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Groupmodules/modes/sequentialMode.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Groupmodules/topicTitleManager.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "Logmodules/log.html": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Logmodules/log.js": { + "windowReferences": 4, + "documentReferences": 32, + "electronReferences": 1, + "timers": 5, + "listeners": 18, + "mutableMaps": 0 + }, + "Loommodules/device-menu.html": { + "windowReferences": 1, + "documentReferences": 5, + "electronReferences": 0, + "timers": 0, + "listeners": 3, + "mutableMaps": 0 + }, + "Loommodules/manager.html": { + "windowReferences": 15, + "documentReferences": 9, + "electronReferences": 0, + "timers": 1, + "listeners": 14, + "mutableMaps": 0 + }, + "Loommodules/shell.html": { + "windowReferences": 1, + "documentReferences": 4, + "electronReferences": 0, + "timers": 1, + "listeners": 13, + "mutableMaps": 0 + }, + "Memomodules/memo-graph.js": { + "windowReferences": 12, + "documentReferences": 31, + "electronReferences": 0, + "timers": 3, + "listeners": 1, + "mutableMaps": 0 + }, + "Memomodules/memo-workbench.js": { + "windowReferences": 3, + "documentReferences": 19, + "electronReferences": 1, + "timers": 1, + "listeners": 3, + "mutableMaps": 0 + }, + "Memomodules/memo.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Memomodules/memo.js": { + "windowReferences": 29, + "documentReferences": 115, + "electronReferences": 1, + "timers": 5, + "listeners": 10, + "mutableMaps": 1 + }, + "Musicmodules/music-effects.js": { + "windowReferences": 0, + "documentReferences": 6, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "Musicmodules/music-lyrics.js": { + "windowReferences": 0, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "Musicmodules/music-output.js": { + "windowReferences": 0, + "documentReferences": 5, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music-player.js": { + "windowReferences": 0, + "documentReferences": 2, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music-sidebar.js": { + "windowReferences": 4, + "documentReferences": 11, + "electronReferences": 0, + "timers": 1, + "listeners": 6, + "mutableMaps": 0 + }, + "Musicmodules/music-ui.js": { + "windowReferences": 2, + "documentReferences": 4, + "electronReferences": 0, + "timers": 6, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music-utils.js": { + "windowReferences": 2, + "documentReferences": 2, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music-visualizer.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music-webdav.js": { + "windowReferences": 0, + "documentReferences": 16, + "electronReferences": 0, + "timers": 0, + "listeners": 15, + "mutableMaps": 0 + }, + "Musicmodules/music.html": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Musicmodules/music.js": { + "windowReferences": 5, + "documentReferences": 118, + "electronReferences": 1, + "timers": 2, + "listeners": 8, + "mutableMaps": 1 + }, + "Musicmodules/socket.io.min.js": { + "windowReferences": 2, + "documentReferences": 3, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "Notemodules/notemini.html": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Notemodules/notemini.js": { + "windowReferences": 3, + "documentReferences": 8, + "electronReferences": 1, + "timers": 1, + "listeners": 7, + "mutableMaps": 0 + }, + "Notemodules/notes.html": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Notemodules/notes.js": { + "windowReferences": 46, + "documentReferences": 95, + "electronReferences": 1, + "timers": 13, + "listeners": 62, + "mutableMaps": 1 + }, + "PluginManagerModules/plugin-manager.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "PluginManagerModules/plugin-manager.js": { + "windowReferences": 6, + "documentReferences": 30, + "electronReferences": 1, + "timers": 2, + "listeners": 19, + "mutableMaps": 1 + }, + "Promptmodules/modular-prompt-module.js": { + "windowReferences": 2, + "documentReferences": 55, + "electronReferences": 5, + "timers": 5, + "listeners": 29, + "mutableMaps": 0 + }, + "Promptmodules/original-prompt-module.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 3, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "Promptmodules/preset-prompt-module.js": { + "windowReferences": 4, + "documentReferences": 16, + "electronReferences": 7, + "timers": 4, + "listeners": 1, + "mutableMaps": 0 + }, + "Promptmodules/prompt-manager.js": { + "windowReferences": 10, + "documentReferences": 5, + "electronReferences": 14, + "timers": 1, + "listeners": 3, + "mutableMaps": 0 + }, + "RAGmodules/RAG_Observer.html": { + "windowReferences": 73, + "documentReferences": 28, + "electronReferences": 33, + "timers": 11, + "listeners": 4, + "mutableMaps": 2 + }, + "RAGmodules/RAG_Overlay.html": { + "windowReferences": 16, + "documentReferences": 22, + "electronReferences": 12, + "timers": 0, + "listeners": 12, + "mutableMaps": 4 + }, + "RAGmodules/rag-observer-config.js": { + "windowReferences": 23, + "documentReferences": 12, + "electronReferences": 11, + "timers": 3, + "listeners": 8, + "mutableMaps": 0 + }, + "ScriptoriumModules/font-font-test.html": { + "windowReferences": 0, + "documentReferences": 12, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-agent-port.js": { + "windowReferences": 8, + "documentReferences": 11, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 3 + }, + "ScriptoriumModules/scriptorium-async.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "ScriptoriumModules/scriptorium-deck-adapter.js": { + "windowReferences": 1, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-deck-editor.js": { + "windowReferences": 5, + "documentReferences": 19, + "electronReferences": 0, + "timers": 1, + "listeners": 9, + "mutableMaps": 5 + }, + "ScriptoriumModules/scriptorium-deck-export.js": { + "windowReferences": 22, + "documentReferences": 10, + "electronReferences": 0, + "timers": 16, + "listeners": 2, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-deck-renderer.js": { + "windowReferences": 2, + "documentReferences": 3, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-document-store.js": { + "windowReferences": 1, + "documentReferences": 23, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 5 + }, + "ScriptoriumModules/scriptorium-dom-selection.js": { + "windowReferences": 2, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-edit-history.js": { + "windowReferences": 6, + "documentReferences": 8, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-export-resources.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-export.js": { + "windowReferences": 1, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-find.js": { + "windowReferences": 4, + "documentReferences": 2, + "electronReferences": 0, + "timers": 0, + "listeners": 6, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-flow-adapter.js": { + "windowReferences": 1, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-flow-editor.js": { + "windowReferences": 24, + "documentReferences": 21, + "electronReferences": 0, + "timers": 9, + "listeners": 15, + "mutableMaps": 4 + }, + "ScriptoriumModules/scriptorium-flow-export.js": { + "windowReferences": 1, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-flow-renderer.js": { + "windowReferences": 8, + "documentReferences": 5, + "electronReferences": 0, + "timers": 4, + "listeners": 2, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-formatting.js": { + "windowReferences": 7, + "documentReferences": 5, + "electronReferences": 0, + "timers": 1, + "listeners": 9, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-input-sync.js": { + "windowReferences": 8, + "documentReferences": 0, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-library.js": { + "windowReferences": 2, + "documentReferences": 5, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-lineage-store.js": { + "windowReferences": 1, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-lineage-ui.js": { + "windowReferences": 4, + "documentReferences": 22, + "electronReferences": 0, + "timers": 1, + "listeners": 20, + "mutableMaps": 2 + }, + "ScriptoriumModules/scriptorium-media.js": { + "windowReferences": 4, + "documentReferences": 12, + "electronReferences": 0, + "timers": 2, + "listeners": 6, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-navigation.js": { + "windowReferences": 1, + "documentReferences": 12, + "electronReferences": 0, + "timers": 0, + "listeners": 5, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-network-fonts.js": { + "windowReferences": 1, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-objects.js": { + "windowReferences": 3, + "documentReferences": 19, + "electronReferences": 0, + "timers": 1, + "listeners": 12, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-pagination.js": { + "windowReferences": 1, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-pr-diff.js": { + "windowReferences": 1, + "documentReferences": 9, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-pretext-bridge.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "ScriptoriumModules/scriptorium-programmable-content.js": { + "windowReferences": 4, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-render-coordinator.js": { + "windowReferences": 2, + "documentReferences": 6, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-render-primitives.js": { + "windowReferences": 6, + "documentReferences": 11, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-rendered-text.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 5, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-runtime.js": { + "windowReferences": 18, + "documentReferences": 8, + "electronReferences": 0, + "timers": 18, + "listeners": 0, + "mutableMaps": 2 + }, + "ScriptoriumModules/scriptorium-runtime.origin.js": { + "windowReferences": 19, + "documentReferences": 9, + "electronReferences": 0, + "timers": 21, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-session.js": { + "windowReferences": 2, + "documentReferences": 28, + "electronReferences": 0, + "timers": 1, + "listeners": 4, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-settings.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-shell.js": { + "windowReferences": 9, + "documentReferences": 18, + "electronReferences": 0, + "timers": 5, + "listeners": 13, + "mutableMaps": 1 + }, + "ScriptoriumModules/scriptorium-source-editor.js": { + "windowReferences": 14, + "documentReferences": 4, + "electronReferences": 0, + "timers": 5, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-style-ui.js": { + "windowReferences": 1, + "documentReferences": 7, + "electronReferences": 0, + "timers": 0, + "listeners": 10, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-svg-assets.js": { + "windowReferences": 6, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 20, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium-visibility.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 5, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium.html": { + "windowReferences": 3, + "documentReferences": 17, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/scriptorium.js": { + "windowReferences": 91, + "documentReferences": 7, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 1 + }, + "ScriptoriumModules/vdoc-container.js": { + "windowReferences": 1, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 3 + }, + "ScriptoriumModules/vdoc-core.js": { + "windowReferences": 1, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "ScriptoriumModules/vdoc-hybrid-compiler.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "ScriptoriumModules/vdoc-style-library.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "ScriptoriumModules/vdoc-svg-asset-library.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 3 + }, + "Tavernmodules/tavern-manager.js": { + "windowReferences": 25, + "documentReferences": 12, + "electronReferences": 1, + "timers": 1, + "listeners": 13, + "mutableMaps": 1 + }, + "Themesmodules/themes.html": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Themesmodules/themes.js": { + "windowReferences": 5, + "documentReferences": 21, + "electronReferences": 1, + "timers": 0, + "listeners": 6, + "mutableMaps": 0 + }, + "Translatormodules/translator.html": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Translatormodules/translator.js": { + "windowReferences": 3, + "documentReferences": 24, + "electronReferences": 1, + "timers": 4, + "listeners": 12, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 7 + }, + "VCPDistributedServer/Plugin/ChatRoomViewer/ChatRoomViewer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/DeepMemo/DeepMemo.js": { + "windowReferences": 0, + "documentReferences": 8, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/DeepMemo/DeepMemoService.js": { + "windowReferences": 15, + "documentReferences": 10, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/DesktopRemote/desktop-remote.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/DistImageServer/image-server.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/FileOperator/CodeValidator.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/FileOperator/FileOperator.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/LoomController/LoomControllerService.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/MusicController/music-controller.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/OldPowerShellExecutor/PowerShellExecutor.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 6, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/PluginErrorReporter.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.html": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.js": { + "windowReferences": 27, + "documentReferences": 20, + "electronReferences": 0, + "timers": 6, + "listeners": 9, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PluginSourceViewer/PluginSourceViewer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 5, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.html": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js": { + "windowReferences": 31, + "documentReferences": 11, + "electronReferences": 28, + "timers": 2, + "listeners": 12, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 2, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PowerShellExecutor/test_security_check.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/PromptSponsor/prompt-sponsor.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/ScriptoriumCollaborator/ScriptoriumCollaboratorService.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/TopicMemo/TopicMemo.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/TopicSponsor/topicsponsor.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPEverything/local-search-controller.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/config/defaults.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/core/db.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/core/hash.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/core/idempotency.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/core/logger.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/dto/agent.dto.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/dto/group.dto.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/dto/index.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/dto/topic.dto.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/index.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/protocol.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/canonical.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/central.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/sync/projection.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/transport/ndjson.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/transport/routes.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/utils/lock.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPMobileSync/utils/mime.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPSuperDice/example.js": { + "windowReferences": 0, + "documentReferences": 2, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPSuperDice/superdice.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/inventory.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/locate.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/media-server.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js": { + "windowReferences": 13, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaper/we-wallpaper-service.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js": { + "windowReferences": 4, + "documentReferences": 19, + "electronReferences": 0, + "timers": 2, + "listeners": 22, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js": { + "windowReferences": 6, + "documentReferences": 9, + "electronReferences": 1, + "timers": 2, + "listeners": 2, + "mutableMaps": 1 + }, + "VCPDistributedServer/Plugin/VChatDynamicWallpaper/plugin.js": { + "windowReferences": 5, + "documentReferences": 15, + "electronReferences": 1, + "timers": 1, + "listeners": 13, + "mutableMaps": 0 + }, + "VCPDistributedServer/Plugin/WindowSensor/sensor-wrapper.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/VCPDistributedServer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 5, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPDistributedServer/frontend-plugin-loader.js": { + "windowReferences": 3, + "documentReferences": 9, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 1 + }, + "VCPHumanToolBox/ComfyUImodules/ComfyUILoader.js": { + "windowReferences": 7, + "documentReferences": 8, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/ComfyUImodules/ComfyUI_StateManager.js": { + "windowReferences": 7, + "documentReferences": 0, + "electronReferences": 6, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/ComfyUImodules/ComfyUI_UIManager.js": { + "windowReferences": 15, + "documentReferences": 15, + "electronReferences": 6, + "timers": 3, + "listeners": 1, + "mutableMaps": 1 + }, + "VCPHumanToolBox/ComfyUImodules/PathResolver.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/ComfyUImodules/comfyUIConfig.js": { + "windowReferences": 13, + "documentReferences": 3, + "electronReferences": 10, + "timers": 2, + "listeners": 1, + "mutableMaps": 0 + }, + "VCPHumanToolBox/ComfyUImodules/comfyui-ipc.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader.js": { + "windowReferences": 36, + "documentReferences": 8, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditorLoader_Simplified.js": { + "windowReferences": 28, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ApiConfigDialog.js": { + "windowReferences": 1, + "documentReferences": 30, + "electronReferences": 0, + "timers": 5, + "listeners": 10, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_CanvasManager_JSPlumb.js": { + "windowReferences": 9, + "documentReferences": 36, + "electronReferences": 0, + "timers": 22, + "listeners": 25, + "mutableMaps": 2 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_Config.js": { + "windowReferences": 9, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ConnectionManager_Simplified.js": { + "windowReferences": 2, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_ExecutionEngine.js": { + "windowReferences": 17, + "documentReferences": 13, + "electronReferences": 1, + "timers": 5, + "listeners": 2, + "mutableMaps": 7 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager.js": { + "windowReferences": 34, + "documentReferences": 20, + "electronReferences": 0, + "timers": 3, + "listeners": 6, + "mutableMaps": 2 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor.js": { + "windowReferences": 2, + "documentReferences": 2, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLExtractor_Integration.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_NodeManager_URLRenderer_Patch.js": { + "windowReferences": 24, + "documentReferences": 24, + "electronReferences": 0, + "timers": 0, + "listeners": 15, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginDialog.js": { + "windowReferences": 17, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 4, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_PluginManager.js": { + "windowReferences": 5, + "documentReferences": 14, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 2 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_StateManager.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 7 + }, + "VCPHumanToolBox/WorkflowEditormodules/WorkflowEditor_UIManager.js": { + "windowReferences": 36, + "documentReferences": 76, + "electronReferences": 0, + "timers": 14, + "listeners": 33, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/ai/AiClientFactory.js": { + "windowReferences": 8, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/ai/HttpAiClient.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/WorkflowEditormodules/jsplumb.min.js": { + "windowReferences": 52, + "documentReferences": 36, + "electronReferences": 0, + "timers": 5, + "listeners": 1, + "mutableMaps": 0 + }, + "VCPHumanToolBox/index.html": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/main.js": { + "windowReferences": 10, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/preload.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 2, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/renderer.js": { + "windowReferences": 39, + "documentReferences": 123, + "electronReferences": 7, + "timers": 7, + "listeners": 36, + "mutableMaps": 1 + }, + "VCPHumanToolBox/renderer_modules/config.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VCPHumanToolBox/renderer_modules/tool-manager.js": { + "windowReferences": 9, + "documentReferences": 40, + "electronReferences": 3, + "timers": 1, + "listeners": 27, + "mutableMaps": 0 + }, + "VCPHumanToolBox/renderer_modules/ui/canvas-editor.js": { + "windowReferences": 1, + "documentReferences": 45, + "electronReferences": 0, + "timers": 5, + "listeners": 30, + "mutableMaps": 0 + }, + "VCPHumanToolBox/renderer_modules/ui/canvas-handler.js": { + "windowReferences": 0, + "documentReferences": 14, + "electronReferences": 0, + "timers": 4, + "listeners": 10, + "mutableMaps": 0 + }, + "VCPHumanToolBox/renderer_modules/ui/dynamic-image-handler.js": { + "windowReferences": 0, + "documentReferences": 15, + "electronReferences": 0, + "timers": 1, + "listeners": 10, + "mutableMaps": 0 + }, + "VCP文坊超级引擎-四重动画实证.html": { + "windowReferences": 26, + "documentReferences": 23, + "electronReferences": 0, + "timers": 26, + "listeners": 3, + "mutableMaps": 1 + }, + "VchatManager/consistency-checker.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VchatManager/index.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VchatManager/main.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VchatManager/preload.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "VchatManager/script.js": { + "windowReferences": 15, + "documentReferences": 64, + "electronReferences": 0, + "timers": 1, + "listeners": 24, + "mutableMaps": 0 + }, + "Voicechatmodules/recognizer.html": { + "windowReferences": 11, + "documentReferences": 3, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "Voicechatmodules/voicechat.html": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "Voicechatmodules/voicechat.js": { + "windowReferences": 34, + "documentReferences": 24, + "electronReferences": 26, + "timers": 7, + "listeners": 13, + "mutableMaps": 0 + }, + "WebIndexTTS2/public/app.js": { + "windowReferences": 0, + "documentReferences": 32, + "electronReferences": 0, + "timers": 0, + "listeners": 10, + "mutableMaps": 0 + }, + "WebIndexTTS2/public/index.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "WebIndexTTS2/server.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "main.html": { + "windowReferences": 4, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "main.js": { + "windowReferences": 28, + "documentReferences": 0, + "electronReferences": 0, + "timers": 10, + "listeners": 0, + "mutableMaps": 0 + }, + "migration/migrateAvatars.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/SovitsTTS.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/assistant/assistant-rust-adapter.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatContext.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatDomRenderer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatEventContract.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatHistoryMutationAuthority.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/chat/chatHistoryPersistence.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatOperation.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatPluginManifest.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "modules/chat/chatPresentationSkin.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatPresentationState.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatRepository.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 5, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatSurface.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/chatSurfaceSlots.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/chat/chatThemePlugin.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/contentModes.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/contentRuntime.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/contentTransforms.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/mainChatStateAuthority.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/memoryChatRepository.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/streamConsumerRegistry.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/chat/streamCoordinator.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 4 + }, + "modules/chat/streamSession.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/streamTransientHistory.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/chat/surfaceConversation.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/chat/vcpStreamBridge.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 3 + }, + "modules/chatManager.js": { + "windowReferences": 17, + "documentReferences": 14, + "electronReferences": 45, + "timers": 1, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/contextSanitizer.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/emoticonManager.js": { + "windowReferences": 4, + "documentReferences": 6, + "electronReferences": 1, + "timers": 1, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/event-listeners.js": { + "windowReferences": 25, + "documentReferences": 95, + "electronReferences": 8, + "timers": 1, + "listeners": 62, + "mutableMaps": 0 + }, + "modules/fileManager.js": { + "windowReferences": 0, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/filterManager.js": { + "windowReferences": 3, + "documentReferences": 73, + "electronReferences": 5, + "timers": 0, + "listeners": 11, + "mutableMaps": 0 + }, + "modules/global-settings-manager.js": { + "windowReferences": 8, + "documentReferences": 80, + "electronReferences": 1, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/image-viewer.html": { + "windowReferences": 7, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/image-viewer.js": { + "windowReferences": 12, + "documentReferences": 50, + "electronReferences": 1, + "timers": 6, + "listeners": 38, + "mutableMaps": 0 + }, + "modules/inputEnhancer.js": { + "windowReferences": 4, + "documentReferences": 3, + "electronReferences": 4, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/interruptHandler.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 6, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/agentHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/assistantHandlers.js": { + "windowReferences": 8, + "documentReferences": 2, + "electronReferences": 0, + "timers": 5, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/canvasHandlers.js": { + "windowReferences": 6, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/chatHandlers.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 3 + }, + "modules/ipc/deepWikiHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/desktopHandlers.js": { + "windowReferences": 23, + "documentReferences": 0, + "electronReferences": 0, + "timers": 6, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/desktopMetrics.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 6 + }, + "modules/ipc/desktopRemoteHandlers.js": { + "windowReferences": 11, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 0, + "mutableMaps": 2 + }, + "modules/ipc/diceHandlers.js": { + "windowReferences": 8, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/docxHandlers.js": { + "windowReferences": 1, + "documentReferences": 5, + "electronReferences": 0, + "timers": 3, + "listeners": 2, + "mutableMaps": 1 + }, + "modules/ipc/emoticonHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/fileDialogHandlers.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/forumHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/groupChatHandlers.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/ipcContracts.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/libreHardwareMonitorBridge.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ipc/memoHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/musicHandlers.js": { + "windowReferences": 15, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/notesHandlers.js": { + "windowReferences": 5, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/promptHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/ragHandlers.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/regexHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/settingsHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/sovitsHandlers.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/tavernHandlers.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/themeHandlers.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/translatorHandlers.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/voiceHandlers.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ipc/windowHandlers.js": { + "windowReferences": 20, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/itemListManager.js": { + "windowReferences": 15, + "documentReferences": 19, + "electronReferences": 23, + "timers": 4, + "listeners": 5, + "mutableMaps": 2 + }, + "modules/loom/VCPLoomManager.js": { + "windowReferences": 71, + "documentReferences": 12, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 8 + }, + "modules/loom/webcore/adapter-contract.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/loom/webcore/chrome-adapter.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 2 + }, + "modules/loom/webcore/electron-adapter.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/loom/webcore/index.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/loom/webcore/web-agent-page-core.js": { + "windowReferences": 3, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/loom/webcore/web-agent-page-runtime-core.js": { + "windowReferences": 4, + "documentReferences": 8, + "electronReferences": 0, + "timers": 6, + "listeners": 0, + "mutableMaps": 6 + }, + "modules/loom/webcore/web-agent-protocol.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/loom/webcore/web-agent-runtime-core.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/lyricFetcher.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/mainChatCommands.js": { + "windowReferences": 24, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/messageRenderer.js": { + "windowReferences": 6, + "documentReferences": 13, + "electronReferences": 17, + "timers": 13, + "listeners": 7, + "mutableMaps": 6 + }, + "modules/modelUsageTracker.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/musicScannerWorker.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/notificationRenderer.js": { + "windowReferences": 6, + "documentReferences": 20, + "electronReferences": 1, + "timers": 1, + "listeners": 3, + "mutableMaps": 0 + }, + "modules/renderer/animation.js": { + "windowReferences": 61, + "documentReferences": 29, + "electronReferences": 0, + "timers": 24, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/renderer/colorUtils.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/renderer/contentPipeline.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/renderer/contentProcessor.js": { + "windowReferences": 8, + "documentReferences": 20, + "electronReferences": 0, + "timers": 4, + "listeners": 4, + "mutableMaps": 0 + }, + "modules/renderer/desktopPushConsumer.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 7, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/renderer/domBuilder.js": { + "windowReferences": 1, + "documentReferences": 3, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/domListenerOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/renderer/emoticonUrlFixer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 3, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/enhancedColorUtils.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/renderer/forwardMessageOwner.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/imageHandler.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 4, + "timers": 0, + "listeners": 2, + "mutableMaps": 1 + }, + "modules/renderer/mainChatAttachmentOwner.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatAuxiliaryEventOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatComposition.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatDomBindings.js": { + "windowReferences": 1, + "documentReferences": 56, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatEventBridge.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatFlowlockOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatSendOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatSettingsOwner.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatSettingsPresentationOwner.js": { + "windowReferences": 0, + "documentReferences": 68, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatStreamConsumer.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatSurfaceAdapter.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/mainChatThemeOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/messageContextMenu.js": { + "windowReferences": 1, + "documentReferences": 1, + "electronReferences": 36, + "timers": 1, + "listeners": 3, + "mutableMaps": 0 + }, + "modules/renderer/middleClickHandler.js": { + "windowReferences": 3, + "documentReferences": 10, + "electronReferences": 14, + "timers": 9, + "listeners": 8, + "mutableMaps": 2 + }, + "modules/renderer/nonStreamingEventConsumer.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/ownedPreloadSubscription.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/pretext-bridge.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/renderer/pretext.bundle.js": { + "windowReferences": 0, + "documentReferences": 7, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/renderer/pretext.esm.js": { + "windowReferences": 0, + "documentReferences": 7, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/renderer/renderDependencies.js": { + "windowReferences": 0, + "documentReferences": 2, + "electronReferences": 5, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/renderSessionAuthority.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/streamManager.js": { + "windowReferences": 4, + "documentReferences": 1, + "electronReferences": 3, + "timers": 5, + "listeners": 2, + "mutableMaps": 1 + }, + "modules/renderer/streamProjectionRuntime.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "modules/renderer/surfaceTaskOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/toolRequestScanner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/topicSelectionReadiness.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/ttsSurfaceOwner.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/renderer/visibilityOptimizer.js": { + "windowReferences": 13, + "documentReferences": 0, + "electronReferences": 0, + "timers": 9, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/renderer/windowStreamRuntime.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/searchManager.js": { + "windowReferences": 1, + "documentReferences": 28, + "electronReferences": 7, + "timers": 4, + "listeners": 8, + "mutableMaps": 0 + }, + "modules/services/chatDataService/client.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 2, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/services/chatDataService/index.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/chatDataService/lifecycle.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 4, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/deepWikiService.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/embeddedAppSessionManager.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 1, + "mutableMaps": 2 + }, + "modules/services/historyWatcherLeaseManager.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/networkNotesCacheStore.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 3 + }, + "modules/services/preloadPaths.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/scriptoriumAgentControlService.js": { + "windowReferences": 2, + "documentReferences": 3, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/scriptoriumFontCacheService.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 4 + }, + "modules/services/scriptoriumImportService.js": { + "windowReferences": 0, + "documentReferences": 1, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 3 + }, + "modules/services/scriptoriumPptxImportService.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/services/senderTaskRegistry.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 2 + }, + "modules/services/windowAppIds.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/services/windowService.js": { + "windowReferences": 9, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/services/windowStateService.js": { + "windowReferences": 2, + "documentReferences": 1, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/settingsManager.js": { + "windowReferences": 30, + "documentReferences": 109, + "electronReferences": 50, + "timers": 11, + "listeners": 39, + "mutableMaps": 1 + }, + "modules/shared/embeddedAppAllowlist.js": { + "windowReferences": 11, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/speechRecognizer.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/tavernRulesEngine.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/text-viewer.html": { + "windowReferences": 7, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/text-viewer.js": { + "windowReferences": 46, + "documentReferences": 72, + "electronReferences": 2, + "timers": 5, + "listeners": 31, + "mutableMaps": 2 + }, + "modules/topTabManager.js": { + "windowReferences": 2, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/topicListManager.js": { + "windowReferences": 5, + "documentReferences": 57, + "electronReferences": 25, + "timers": 2, + "listeners": 3, + "mutableMaps": 0 + }, + "modules/topicSummarizer.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/trayManager.js": { + "windowReferences": 34, + "documentReferences": 26, + "electronReferences": 4, + "timers": 3, + "listeners": 6, + "mutableMaps": 0 + }, + "modules/ui-helpers.js": { + "windowReferences": 7, + "documentReferences": 54, + "electronReferences": 0, + "timers": 7, + "listeners": 10, + "mutableMaps": 2 + }, + "modules/ui-system/appearance-engine.js": { + "windowReferences": 4, + "documentReferences": 20, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/appearance-studio.js": { + "windowReferences": 27, + "documentReferences": 56, + "electronReferences": 1, + "timers": 3, + "listeners": 5, + "mutableMaps": 0 + }, + "modules/ui-system/ask-nova-modal.js": { + "windowReferences": 20, + "documentReferences": 7, + "electronReferences": 1, + "timers": 3, + "listeners": 4, + "mutableMaps": 2 + }, + "modules/ui-system/avatar-picker.js": { + "windowReferences": 1, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/component-manifest.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/component-showcase.js": { + "windowReferences": 1, + "documentReferences": 33, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/contribution-registry.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ui-system/interactive-chat-app.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/lifecycle-inspector.js": { + "windowReferences": 2, + "documentReferences": 1, + "electronReferences": 1, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/lifecycle-scope.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 4, + "listeners": 1, + "mutableMaps": 1 + }, + "modules/ui-system/lucide-adapter.js": { + "windowReferences": 4, + "documentReferences": 4, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/account-menu-controller.js": { + "windowReferences": 7, + "documentReferences": 13, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/app-tab-host.js": { + "windowReferences": 0, + "documentReferences": 26, + "electronReferences": 0, + "timers": 2, + "listeners": 1, + "mutableMaps": 1 + }, + "modules/ui-system/next-shell/assistant-search-controller.js": { + "windowReferences": 0, + "documentReferences": 11, + "electronReferences": 0, + "timers": 1, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/creation-controller.js": { + "windowReferences": 13, + "documentReferences": 9, + "electronReferences": 1, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/embedded-app-controller.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/escape-dispatcher.js": { + "windowReferences": 0, + "documentReferences": 6, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/launchpad-controller.js": { + "windowReferences": 1, + "documentReferences": 12, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/next-shell-controller.js": { + "windowReferences": 71, + "documentReferences": 43, + "electronReferences": 2, + "timers": 3, + "listeners": 2, + "mutableMaps": 2 + }, + "modules/ui-system/next-shell/notification-menu-controller.js": { + "windowReferences": 10, + "documentReferences": 11, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/next-shell/overlay-coordinator.js": { + "windowReferences": 0, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 1 + }, + "modules/ui-system/next-ui-apps.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/performance-recorder.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/settings-bridge.js": { + "windowReferences": 7, + "documentReferences": 29, + "electronReferences": 0, + "timers": 0, + "listeners": 6, + "mutableMaps": 1 + }, + "modules/ui-system/settlement.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/sidebar-resizer.js": { + "windowReferences": 1, + "documentReferences": 2, + "electronReferences": 0, + "timers": 1, + "listeners": 5, + "mutableMaps": 0 + }, + "modules/ui-system/standalone-chat-app.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/startup-theme-gate.js": { + "windowReferences": 0, + "documentReferences": 5, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/state-channel.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ui-system/surface-controller.js": { + "windowReferences": 8, + "documentReferences": 5, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/task-handle.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/ui-system/ui-surface-policy.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/ui-system/vcp-main-ui-runtime.js": { + "windowReferences": 4, + "documentReferences": 9, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 0 + }, + "modules/ui-system/vcp-ui.js": { + "windowReferences": 16, + "documentReferences": 126, + "electronReferences": 0, + "timers": 6, + "listeners": 18, + "mutableMaps": 4 + }, + "modules/ui-system/webawesome-adapter.js": { + "windowReferences": 4, + "documentReferences": 10, + "electronReferences": 0, + "timers": 0, + "listeners": 2, + "mutableMaps": 1 + }, + "modules/ui-system/webawesome-comparison.js": { + "windowReferences": 0, + "documentReferences": 7, + "electronReferences": 0, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "modules/ui-system/webawesome-runtime-manifest.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/uiManager.js": { + "windowReferences": 20, + "documentReferences": 12, + "electronReferences": 16, + "timers": 2, + "listeners": 13, + "mutableMaps": 0 + }, + "modules/utils/agentConfigManager.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 5 + }, + "modules/utils/appSettingsManager.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 3, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/vcpClient.js": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 1, + "listeners": 0, + "mutableMaps": 1 + }, + "modules/weatherService.js": { + "windowReferences": 3, + "documentReferences": 11, + "electronReferences": 1, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "modules/webdavManager.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preload.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/chat.js": { + "windowReferences": 23, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/desktop.js": { + "windowReferences": 23, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/docx.js": { + "windowReferences": 8, + "documentReferences": 2, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/loom-page.js": { + "windowReferences": 4, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/loom.js": { + "windowReferences": 3, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/shared/apiFactory.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 1, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/shared/catalog.js": { + "windowReferences": 24, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/shared/roles.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "preloads/utility.js": { + "windowReferences": 27, + "documentReferences": 9, + "electronReferences": 1, + "timers": 0, + "listeners": 1, + "mutableMaps": 0 + }, + "renderer.js": { + "windowReferences": 118, + "documentReferences": 51, + "electronReferences": 12, + "timers": 1, + "listeners": 0, + "mutableMaps": 0 + }, + "rust_assistant_engine/ui/assistant-bar.html": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "rust_assistant_engine/ui/assistant-bar.js": { + "windowReferences": 4, + "documentReferences": 8, + "electronReferences": 4, + "timers": 0, + "listeners": 3, + "mutableMaps": 0 + }, + "rust_assistant_engine/ui/assistant.html": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "rust_assistant_engine/ui/assistant.js": { + "windowReferences": 33, + "documentReferences": 20, + "electronReferences": 21, + "timers": 0, + "listeners": 7, + "mutableMaps": 0 + }, + "rust_chat_data_service/build-runtime.js": { + "windowReferences": 0, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "splash.html": { + "windowReferences": 1, + "documentReferences": 0, + "electronReferences": 0, + "timers": 0, + "listeners": 0, + "mutableMaps": 0 + }, + "test.html": { + "windowReferences": 2, + "documentReferences": 14, + "electronReferences": 0, + "timers": 2, + "listeners": 5, + "mutableMaps": 0 + } + }, + "invariants": [ + "pure kernel files have no document/window/electronAPI dependency", + "production and test consumers are reported separately", + "provider definitions, exports and compatibility assignments cannot self-certify as consumers", + "production consumers and compatibility globals are discovered across all repository production sources", + "each consumer evidence item records a source line, member access and production surface", + "legacy facade removal requires a later zero-production-consumer proof", + "retired renderer globals have zero production definitions and consumers", + "each retained public facade is frozen, has production consumers, a dynamic smoke and a retirement decision", + "all direct production window facade publications are inventoried with definitions and consumers", + "main-window start/data/end/error events have one coordinator authority" + ] +} diff --git a/docs/chat-kernel-deep-decoupling-roadmap.md b/docs/chat-kernel-deep-decoupling-roadmap.md new file mode 100644 index 000000000..4f0c094af --- /dev/null +++ b/docs/chat-kernel-deep-decoupling-roadmap.md @@ -0,0 +1,45 @@ +# Chat Kernel D0-D7 当前执行合同 + +更新日期:2026-08-21。本文只定义仍然有效的目标、边界和退出条件;阶段状态与最新测试数字只在 [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md) 维护。按时间追加的施工记录已归档到 [`archive/2026-08-chat-kernel-and-ui-roadmaps/`](./archive/2026-08-chat-kernel-and-ui-roadmaps/)。 + +## 最终目标 + +把聊天运行时稳定分为 Domain、Content Runtime 和 Surface Adapter:实时协调、持久事实与 DOM projection 各有唯一 owner;`renderer.js` 只组装主窗口依赖和生命周期;`messageRenderer.js` 只通过显式 root、render model 和 capability closure 投影;无生产消费者的 ambient facade 必须删除。 + +必须保持主聊天发送、流式、取消、重试、历史与话题切换、附件、主题与设置、通知、desktop push、VoiceChat、Rust Assistant、Classic 子页面和插件协议。未登记的 IPC、持久化、terminal、DOM、焦点、ARIA、监听器或资源差异默认视为回归。 + +## 阶段合同 + +| 阶段 | 退出条件 | +| --- | --- | +| D0 | 冻结 renderer、message renderer、chat manager、stream manager 的真实 producer/consumer、owner、失败入口与行为基线。 | +| D1 | StreamSession/StreamState 不依赖 DOM/Electron,明确 operation identity、chunk 顺序、单 terminal 和迟到结果规则。 | +| D2 | Stream Coordinator 唯一拥有 reader、abort、terminal arbitration 和 per-conversation persistence queue;dispose 等待真实工作停稳。 | +| D3 | 主聊天、独立 Surface 和辅助窗口拥有隔离的 stream consumer、conversation identity、projection runtime 与 terminal route。 | +| D4 | MessageRenderer 只消费显式 root、render model、realm/service capability;不得读取主窗口 selection/history ambient state。 | +| D5 | `renderer.js` 只保留 root 查询、capability/provider 构造、owner/adapter mount 和逆序 awaited dispose,不拥有聊天业务 authority。 | +| D6 | 路线指定的仓内 ambient consumers 清零;保留公共 facade 必须冻结、不可替换、有唯一 owner、真实生产消费者、动态 smoke 和退役条件。 | +| D7 | terminal/persistence authority 唯一;完整支持配置矩阵、30-60 分钟人工 soak、逐项审计全部闭合。 | + +## 当前架构决策 + +- 注册即 owned effect。DOM listener、preload subscription、observer、timer 和异步 consumer 必须交给可撤销 owner。 +- `dispose()` 必须幂等并达到 quiescence:先使 generation/abort signal 失效,再撤销 producer,最后等待 in-flight task 和 persistence queue。 +- settings、selection、history 和 attachment snapshot 对消费者是借用值;读取返回 detached copy 或冻结 snapshot,写入只经显式 `set/replace/update/append` authority。 +- event 传播事实变化,不替代当前 snapshot;每个 event 都必须定义 producer、consumer、顺序、terminal 和丢弃规则。 +- 先迁移真实消费者,再删除 facade。静态零引用、动态入口和行为证据缺一不可。 +- 保留的兼容 API 只承担窄命令或只读 projection;不得持有第二份业务状态或生命周期。 + +## 验证合同 + +每个切片至少运行 focused owner tests、`test:chat-kernel`、`test:ui-system`、consumer/Next/design/Classic 门禁和受影响的真实 Electron 入口。最终 D5/D6 还要求主聊天序列、UI Apps/辅助窗口恢复和 lifecycle stress,且 listener/resource 稳定、detached roots 为零。 + +D7 还要求在声明支持的 Windows 版本、安装/打包方式、GPU/DPI 组合上保存逐行证据,并完成人工 soak checklist。当前单主机自动化不能外推为 D7 完成。 + +## 规范来源 + +架构与审查优先遵循 `C:\VCP\vchat-develop\deepseek-harness\AGENTS.md`、`docs/architecture.md`、`docs/event-producer-consumer.md`、`docs/defensive-patterns.md`、`docs/testing.md` 和 `.agents/skills/dsh-code-review/SKILL.md`。这些规范定义审查方法,不替代本仓源码和真实测试证据。 + +## 后续证据工程路线 + +D0-D7 之后的 producer/consumer graph、typed terminal schema、运行 invariant、deliberate invalid runner、source/artifact plane、built-artifact smoke 和 transcript snapshot 统一见 [`chat-kernel-evidence-and-contracts-roadmap.md`](./chat-kernel-evidence-and-contracts-roadmap.md)。该路线增强证据强度,不改变本文的阶段退出条件,也不能替代 D7 的跨环境和人工 soak 证据。 diff --git a/docs/chat-kernel-evidence-and-contracts-roadmap.md b/docs/chat-kernel-evidence-and-contracts-roadmap.md new file mode 100644 index 000000000..6a3ed7631 --- /dev/null +++ b/docs/chat-kernel-evidence-and-contracts-roadmap.md @@ -0,0 +1,180 @@ +# Chat Kernel 证据与契约自动化增强路线 + +更新日期:2026-08-21。本文是 D0-D7 之后的工程化增强路线,不改变当前 D0-D7 的完成判定,也不把缺失的跨 Windows、打包、GPU/DPI 和人工 soak 证据隐含地视为已完成。 + +## 目的与范围 + +本路线把 DeepSeek Harness 的六类可验证性原则应用到 VCPChat:真实 producer/consumer graph、运行关系 invariant、真实 runner 的 invalid-case 回归、source/artifact plane 分离、built-artifact smoke,以及用户可见 transcript snapshot。 + +目标不是把 VCPChat 改造成 Cordis,也不是重新拆 Chat Kernel。目标是让每条架构声明都有机器可执行的证据,并且让门禁在回流发生时主动变红。 + +范围内:Chat Kernel、renderer composition、Surface capability、preload 事件、durable chat history、Electron 入口、Next/Classic/插件协议及其测试和文档。 + +范围外:新的 UI 视觉设计、安装器功能、用户主题数据、`audio_engine/AppData/`、D7 所需但当前机器无法提供的外部 Windows/GPU 环境。 + +## 设计原则 + +- 事实、能力、消费者和生命周期各有唯一 owner;报告引用源码和真实运行证据,不以测试数量替代责任证据。 +- producer/consumer 图由源码生成;手工登记只补充公共协议、动态入口和退役条件,不能覆盖自动发现结果。 +- invariant 必须观察真实事件、持久数据或生命周期计数;服务存在、方法存在、固定纯值和测试替身不能充当运行关系。 +- 每个 invalid-case gate 必须先证明规则会被故意破坏而失败,再恢复实现并保留永久回归测试。 +- source plane 测试源码路径;artifact plane 通过构建产物、bin、Electron 或子进程验证发布路径;两者不能静默混用。 +- 用户可见或模型可见的非平凡变化必须有可重放 snapshot,且 snapshot 通过真实组装入口生成或消费。 +- 所有异步 owner 都要满足 quiescent dispose:失效 generation/abort、撤销 producer、等待 in-flight task 和持久队列,再清理 projection。 + +## 目标证据模型 + +新增统一的 `docs/contracts/` 目录(实现阶段创建),每项记录使用稳定 `id`,至少包含: + +| 字段 | 含义 | +| --- | --- | +| `id` | 稳定契约标识,跨重命名保持不变 | +| `kind` | `event`、`capability`、`facade`、`invariant`、`snapshot`、`artifact-smoke` | +| `owner` | 唯一源码 owner 和生命周期 owner | +| `producer` / `consumers` | 源码位置、运行 surface、是否 durable | +| `mode` | `emit`、`waterfall`、`serial`、`parallel` 或 `command` | +| `terminal` | terminal 类型、单 terminal 规则、迟到/丢弃规则 | +| `sourceEntry` / `artifactEntry` | 源码入口和发布/构建入口 | +| `invalidCase` | 可故意触发的非法状态及预期失败点 | +| `snapshot` | snapshot fixture、归一化规则和审阅 owner | +| `evidence` | 命令、提交、环境、产物和时间戳 | +| `retirement` | facade 或兼容路径的退役条件;永久保留则说明理由 | + +报告分为机器生成部分和人工批准部分。机器部分每次从源码重建;人工部分只保存决策,不复制源码事实。 + +## 路线阶段 + +### E0:基线和证据协议冻结 + +建立 `docs/contracts/README.md`、JSON schema、artifact 命名规则和证据状态:`pass`、`fail`、`skipped`、`manual_required`、`not_applicable`。把当前 `chat-kernel-consumer-report.json`、UI journey matrix、async state matrix 和 D7 审计中的现有证据映射到稳定契约 id。 + +退出条件:schema 校验器能拒绝缺 owner、缺 producer/consumer、缺 terminal 规则或把 `skipped` 当 `pass` 的记录;当前报告可无损转换;不修改产品行为。 + +### E1:真实 producer/consumer graph + +新增 `scripts/build-chat-event-graph.mjs`,扫描 preload producer、`CustomEvent`/IPC dispatch、stream coordinator/session、consumer registry、surface adapters 和明确订阅调用,输出 `docs/contracts/generated/chat-event-graph.json` 与 Mermaid/Markdown 投影。 + +图节点至少区分:durable fact、transient stream event、DOM projection、compatibility facade。边记录源码行、订阅 disposer、conversation/operation identity 和 terminal route。无法静态解析的动态事件必须进入 `undiscovered` 清单并阻止“图完整”声明,而不是被 allowlist 静默吞掉。 + +退出条件: + +- 所有 D0-D6 关键事件都有 producer、consumer、owner 和 disposer; +- 生产者自证、测试-only consumer 和零消费者事件分别报告; +- `undiscovered` 只有有明确协议 owner 的少量条目,并逐项登记; +- 引入一个未登记 producer 或 consumer 会使 gate 失败。 + +### E2:统一事件 terminal schema + +定义 `modules/chat/chatEventContract.js`(或等价的纯模块)及 JSON schema,描述事件名、payload 版本、mode、durability、顺序、terminal、取消、迟到结果和错误归属。`StreamSession`、`StreamCoordinator`、VCP bridge、non-streaming consumer 和 facade command 逐步迁移到同一 vocabulary。 + +先覆盖聊天内核事件,不要求一次性改造所有历史 DOM 事件。旧事件通过显式 adapter 声明 `legacy: true`、owner 和迁移截止条件。 + +退出条件:每个 stream terminal 只能通过 schema 允许的路径提交;重复 terminal、错误 generation、未知 terminal/payload 版本和 dispose 后事件在 unit 与真实 Electron runner 中均被拒绝或丢弃;只有调用方明确提供兼容 fallback 时才允许保留旧 transport 值;错误结果、取消结果和 consumer defect 的公开语义可区分。 + +### E3:运行关系 invariant 与 deliberate invalid runner + +新增 `scripts/run-chat-contract-invariants.mjs` 和测试 fixtures,覆盖: + +- 一个 operation 只能有一个 terminal; +- terminal persistence 只能由唯一 authority 提交; +- 新 conversation identity 不接收旧 operation 的 chunk; +- disposed Surface 不再产生 DOM、通知或持久化副作用; +- facade 必须冻结、不可替换,并由登记 owner 发布; +- listener/resource/detached-root 计数在重复 mount/dispose 后回到基线; +- durable history 与 projection 的成功点和失败点一致。 + +每条 invariant 配套一个真实 runner 的 invalid case:通过 fixture 或受控测试 seam 注入重复 terminal、错误 identity、缺 disposer、可变 facade 或错误 build import,必须在预期 gate 失败。invalid-case 不能只调用被测函数后检查返回值,必须检查外部 DOM、持久文件、事件计数、进程退出码或 artifact 内容。 + +退出条件:每条规则都有“故意破坏为红、恢复为绿”的记录;invalid runner 不依赖生产环境秘密,不改变用户数据;invariant 报告分别显示 `timedOut`、`signal`、`exitCode` 和断言失败。 + +### E4:facade owner registry 和回流门禁 + +把 `scripts/check-chat-kernel-consumers.mjs` 的 facade ledger 与 `docs/contracts/chat-contracts.json` 的 `kind=facade` 登记对齐为 schema-backed registry:每个 facade 记录定义者、唯一 owner、生产消费者、公共方法、冻结性、动态 smoke、退役条件和兼容版本;gate 必须验证登记 owner 与 consumer report 的源码定义一致。 + +新增负向测试:尝试替换冻结 facade、由非 owner 发布同名 facade、增加未登记 `window.*` 业务读取、删除最后一个真实消费者、或让 facade 持有第二份状态时,gate 必须失败。保留 facade 只能是窄命令或只读 projection;新内部功能不得直接依赖它。 + +退出条件:所有直接 `window.*` 发布点出现在 registry;生产消费者和测试消费者分开;provider 不能自证;零生产消费者的 facade 要么删除,要么明确标记 `retirement-blocked` 并有真实 owner 决策。 + +### E5:source/artifact plane 与 built smoke + +定义两套明确入口: + +- source plane:Node test、静态 gate、源码模块导入,统一从 `modules/`/`scripts/` 读取; +- artifact plane:构建后 Electron、CLI/bin、子进程和打包目录,只读取 `dist/`/`lib/`/安装布局中的文件。packaged smoke 拒绝把 workspace source root 当作 packaged root,并要求独立的 `resources/app/main.js`;当前仍只完成 unpacked 文件树检查,不等价于 packaged runtime 启动。 + +新增 `scripts/check-artifact-plane.mjs`,检查测试命令是否错误地从旧 `node_modules`、源码路径或 stale build 读取;新增最小 built smokes: + +- 构建后的主聊天 composition boot; +- 构建后的 stream terminal/persistence replay; +- 构建后的 Next/Classic 页面入口; +- packaged/unpacked Electron 中的 embedded app security 和 crash/reload(当前脚本只完成 unpacked artifact 文件树/Web Awesome smoke,真实 Electron 启动与 crash/reload 仍是未闭合证据); +- 缺失配置、错误导出、错误协议版本必须以非零退出并产生可定位诊断。 + +退出条件:干净 checkout 删除旧 build 后,source gate 和 artifact smoke 都能独立运行;构建产物缺失或错误导出不会被 source loader 掩盖;CI artifact smoke 保存 commit、Node/Electron 版本、入口、退出码和 stderr。 + +### E6:chat-visible/model-visible transcript snapshot + +新增真实组装入口驱动的 snapshot harness。第一批覆盖:发送消息、流式 chunk/terminal、取消/错误、历史切换、附件、主题/设置投影、通知/desktop push、Classic 页面和插件命令协议。snapshot 分三层: + +1. durable transcript:持久 history/event 记录; +2. projection transcript:DOM/ARIA/focus/notification 语义快照; +3. wire transcript:IPC/preload/plugin 命令和 terminal 次序。 + +当前 stream fixtures 同时锁定 `modelVisible` 和 `chatVisible` 两层,再与 durable、projection、wire transcript 对照。归一化只处理随机 id、时间和平台路径;不得删除顺序、状态、错误归属、ARIA 或资源计数。fixture 必须可在 Linux/macOS replay;Windows/Electron 特有结果单独标记平台字段。 + +退出条件:每个非平凡用户可见或模型可见改动在 PR 中新增/更新 snapshot;snapshot 通过真实 composition 或 Electron runner 生成;review 能看到语义 diff;录制和 replay 命令分离,CI 只 replay 不写 fixture。 + +### E7:统一 CI gate、报告和发布证据 + +把上述步骤组合为: + +```text +npm run check:chat-contracts + ├─ schema + generated graph freshness + ├─ facade owner/consumer gate + ├─ invalid-case runner + ├─ source-plane tests + ├─ artifact-plane built smokes + ├─ transcript snapshot replay + └─ evidence manifest validation +``` + +发布环境使用 `npm run check:chat-release-evidence`。该入口复用同一 manifest,但强制要求主聊天、UI Apps、packaged artifact、Windows matrix 和 manual soak 五类证据均为 `passed`;开发期 `npm run check:chat-evidence` 允许明确的 `skipped/manual_required`,两者不能混用。 + +CI 必须把 `fail` 与 `manual_required/skipped` 分开;任何未解释的 skipped 不能使 D7 或发布 gate 变绿。每次运行保存 machine-readable evidence manifest,并在 PR summary 显示变更契约、图差异、snapshot diff、artifact 版本和缺失平台。强制设置 `VCPCHAT_REAL_ELECTRON_STATUS=pass` 也不能覆盖已提供的 packaged artifact 失败或 Windows matrix 失败/跳过。 + +退出条件:干净 checkout、PR、Windows 主机和打包产物都能生成同一 schema 的证据;CI 同时上传 manifest、packaged smoke evidence 与 contracts/graph/transcript evidence bundle;Windows matrix 和 manual-soak JSON 可被 manifest 校验并保留 `fail_or_skipped`/`manual_observation_required` 语义;报告可以反查源码行、测试、artifact 和审阅决策;故意引入 producer 回流、错误 terminal 或 stale artifact 时 CI 确实失败。 + +## 推荐实施顺序 + +1. E0 schema/命名/manifest(只加文档和验证器)。 +2. E1 graph 生成,先消费现有 `chat-kernel-consumer-report`,不改业务事件。 +3. E4 facade registry,与现有静态门禁合并,优先防回流。 +4. E2 terminal schema,先覆盖 StreamSession/Coordinator 和 VCP bridge。 +5. E3 invalid runner 与 lifecycle invariant。 +6. E5 artifact plane/built smoke,修复 CI 的 `npm ci`、Electron runtime 和 stale build 风险。 +7. E6 transcript snapshots,优先主聊天和 stream,再扩展 UI Apps/Classic/插件。 +8. E7 统一 gate 和 PR evidence summary。 + +顺序理由:先建立事实模型和防回流,再约束运行协议;先让 invalid case 能变红,再扩大 snapshot 和 artifact 覆盖,避免生成大量无法解释的历史 fixture。 + +## 证据与失败处理 + +- 所有产物写入 `artifacts/contracts//`,不提交临时日志、用户主题或音频数据。 +- 超时、进程信号、非零退出、断言失败和测试基础设施故障分别记录;重试不能把产品失败改写成通过。 +- 平台未提供时使用 `manual_required` 或 `skipped`,并记录原因、命令和替代证据;不得用 Linux 结果外推 Windows/GPU/DPI 结论。 +- 任何新兼容桥必须登记 owner、消费者、退役条件和删除前置证据;没有这些信息就阻止合入。 + +## 与当前 D0-D7 的关系 + +E0-E4 可以在当前 PR 之后独立推进,不改变 D5/D6 已有结论。E5-E6 是提升发布信心的工程化工作,但不能替代 D7 的多环境和人工 soak。只有 E7 统一 gate、声明支持的环境证据和人工 checklist 都齐全后,才可以把“架构声明有自动化证据”作为发布级能力,而不是仅作为开发者本地工具。 + +## 首个增量 PR 的完成标准 + +首个实现切片已从 E0+E1 扩展为 E0-E7 自动化框架与门禁:schema、graph、terminal vocabulary、runtime invariant、deliberate invalid runner、artifact/source plane、built smoke、transcript snapshot、CI gate 和 machine-readable manifest 均已落地。正向 packaged filesystem smoke 已接入 gate 并写出 `kind=unpacked-packaged-artifact-filesystem-smoke` 证据,但没有 `VCPCHAT_PACKAGED_ROOT` 时只能报告 `skipped`;真实 packaged Electron 启动/crash-reload、跨平台发布矩阵、稳定的多轮 Electron 重跑、成功的 packaged native rebuild 和人工 soak 仍属于 D7 未闭合证据,不能用自动 gate 或单次成功替代它们。 + +## 当前施工状态 + +2026-08-21:E0-E7 自动化框架与门禁已实现;当前 graph 扫描 268 个生产源码文件、191 个事件节点,1 个动态事件点已通过 contract `dynamicSites` 登记,未登记动态点为 0;E2 已为 StreamSession/Coordinator 建立第一版 terminal vocabulary;E3 已有 terminal、facade registry、missing-runtime 和 packaged-root 四类真实 invalid runner;E4 已接入 schema-backed facade owner/消费者校验;E5 已加入 Electron runtime/source-entry、Rust built-artifact `--help` 和外部 unpacked packaged-root filesystem smoke;E6 已加入 modelVisible/chatVisible 与 completed/cancelled/failed/discarded durable/projection/wire transcript replay;E7 的统一 CI evidence gate、正向 packaged smoke(无输入时显式 skipped)、manifest 路径校验、release evidence gate 和 manifest invalid-case 测试已接入 `chat_kernel_ui.yml`。真实 packaged runtime、完整跨平台 artifact 归档、成功的 packaged native rebuild、更多 Electron transcript 和发布环境证据仍待后续增量补齐。 + +本机真实 Electron 证据在修正过时的 Web Awesome fallback 断言后,当前工作树 Windows matrix `artifacts/windows-matrix/2026-08-21T18-36-55-200Z.json` 六行全部通过,包含 60-cycle lifecycle;30 分钟 manual-soak 观察已生成 `artifacts/manual-soak/2026-08-21T18-43-55-291Z.json`,30 个采样保持 renderer errors 为空、detached roots 为 0,但 checklist 尚未由操作员填写,stderr 仍有受控 bad-port/degraded 配置提示,因此状态保持 `manual_observation_required`。临时 Electron unpacked package 构建记录在 `artifacts/packaged/2026-08-21T19-21-58-000Z.json`,因 `electron-edge-js` MSBuild exit 1 未生成产物,packaged 状态保持 `failed`。主聊天 sequence 最近一次通过 24 actions / 25 requests,且 owner focused contract 已覆盖 `aria-busy` 投影,但历史重跑仍出现 `Runtime.callFunctionOn`/残留进程树问题。因此真实 Electron 单项仍标记为 `manual_required`,不把一次成功或单机矩阵外推为跨环境稳定 PASS。manifest 默认保留未闭合项,只有显式设置 `VCPCHAT_REAL_ELECTRON_STATUS=pass` 才会报告为通过。 diff --git a/docs/chat-kernel-vd7-final-audit.md b/docs/chat-kernel-vd7-final-audit.md new file mode 100644 index 000000000..10c1561f6 --- /dev/null +++ b/docs/chat-kernel-vd7-final-audit.md @@ -0,0 +1,66 @@ +# Chat Kernel vD0-vD7 Final Audit + +审计日期:2026-08-21。本文是发布前状态审计,不是整个路线完成声明。vD5/vD6 已达到当前退出条件;vD7 仍缺人工 soak 和跨 Windows/打包/GPU-DPI 证据,因此不得标记 D0-vD7 最终完成。 + +| 阶段 | 退出条件 | 当前证据 | 状态 | +| --- | --- | --- | --- | +| vD0 | owner、producer、consumer、失败入口和工作树边界冻结 | `docs/chat-kernel-consumer-report.json`、当前执行合同、当前 git 状态 | PASS(工作树仍含用户改动) | +| vD1 | StreamSession/State 无 DOM/Electron,terminal/generation/late-event 可复现 | `npm run test:chat-kernel` 146/146 | PASS | +| vD2 | coordinator 等待真实 reader、terminal 和 persistence,dispose 达到 quiescence | coordinator/history/operation tests、lifecycle stress | PASS | +| vD3 | 主聊天、独立 Surface、VoiceChat、Rust Assistant 有真实隔离 consumer | 主聊天序列、辅助 reload/crash matrix、consumer gate | PASS(当前主机真实入口) | +| vD4 | RenderDependencies、root、render model、capability closure 显式化 | render dependency/surface/projection tests、Next delta gate | PASS | +| vD5 | `renderer.js` 只作主聊天 composition/lifecycle root;业务状态、副作用和资源由 named owner 持有 | owner modules、renderer responsibility gate、UI/主聊天 Electron、本补丁 20-cycle lifecycle | PASS | +| vD6 | 本路线目标的内部 ambient consumers 清零;保留 facade 窄化、冻结、唯一 owner、有真实消费者和动态 smoke | facade ledger、retired-global negative gate、Electron UI Apps/Next/Classic | PASS(范围限定) | +| vD7 | 唯一 terminal/persistence authority、发布配置矩阵、30-60 分钟人工 soak、逐项审计 | 当前主机矩阵、辅助恢复、manual-soak harness | BLOCKED:缺多版本/打包/GPU-DPI Windows 证据;缺人工 checklist 记录 | + +## vD5 责任审计 + +- `renderer.js` 只保留 DOM root 查询、provider/capability 构造、manager/adapter/owner 装配、mount 和逆序 dispose。 +- 设置 projection、表单同步、presentation normalize/apply、字体和气泡布局由 `MainChatSettingsPresentationOwner` 持有;主题、TTS、Flowlock、辅助 preload 事件、forward modal、DOM listener、附件瞬态状态和发送/中止策略分别由 named owner 持有。 +- `MainChatAttachmentOwner` 提供唯一稳定 attachment ref 和 preview projection;`MainChatSendOwner` 持有 send/interrupt DOM/ARIA、interruptible message 选择、group/agent 中止分派及失败时 coordinator-owned local cancel。`renderer.js` 只注入 capability,并按注册逆序等待所有 owner dispose。 +- `renderer.js` 不拥有 stream terminal、durable persistence、TTS queue、history cache 或 preload business-event 状态;静态门禁禁止这些责任回流。 +- preload subscription、ChatManager 与 UIManager 的异步 consumer/observer/theme subscription 现在都是 owned effect。dispose 会先 abort/invalidate generation,再撤销 producer,并等待 in-flight consumer、last-open save 和 outgoing persistence queue;迟到 Canvas/theme/settings 结果不再投影。 +- 本轮真实 Electron 首次暴露 startup restore 在 topic DOM consumer 完成前返回。`ChatManager.selectItem()` 现等待 `topicListManager.loadTopicList()`,并在等待后重新校验 selection generation;回归测试证明 restore 不会早于 topic projection 完成。 +- 对抗式复核发现 selection authority 的冻结值仍被 ChatManager、SettingsManager 和 MessageRenderer 原地写入,附件 ref 存在数组别名竞态,InputEnhancer 与 settings/presentation owner 在 dispose 后仍可能投影迟到结果。现已统一改为 clone-and-set、owned attachment append、tracked async task 与 generation guard。 +- 文件变更触发的 history sync 现捕获 item/topic generations 和 conversation identity;读取返回后只允许仍为当前选择的同步提交 DOM 与 history。选择切换、较新的同步或 dispose 都会使旧同步失效;`chat-manager-selection-race` 覆盖切换与 dispose 两条迟到结果路径。 + +## vD6 facade 审计 + +- 已删除并由负向门禁禁止:`window.globalSettings`、`window.applyChatPresentationMode`、`window.normalizeChatPresentationMode`、`window.checkMessageFilter`、`window.applyChatBubbleLayoutSettings`。 +- 仓内 weather、settings、notification renderer、Next shell、Appearance Studio、Flowlock 和 global settings manager 已改用显式 settings/filter/presentation capability。 +- 保留 `window.VCPMainChatState`、`window.MainChatCommands` 与 `window.VCPAppearanceStudio`。三者均由唯一 owner 以 `Object.defineProperty` 发布,value 冻结且属性不可写、不可配置;`VCPMainChatState` 只暴露只读 snapshot,不包含 mutation refs。生产消费者、公共方法、动态 smoke 和退役条件登记在 consumer report。 +- 测试不再替换公共 facade;临时命令覆盖通过 contribution registry 或显式 test adapter 完成。 +- consumer report 当前清点 164 个直接发布的 ambient facade 名称。除上述三个受支持公共 facade 外,其余统一标记为 `legacy-or-feature-local-ambient` 兼容债务;vD6 PASS 只表示本路线指定的 settings/filter/presentation 与旧 chat/renderer/stream facade 已退休,不表示全仓所有 `window.*` 已消失。 +- facade gate 现在把每个保留 facade 绑定到具体 npm smoke 入口与协议断言片段,不再仅以测试文件存在作为动态覆盖证明。 + +## 当前自动化证据 + +- Chat Kernel:146/146。 +- UI System:97/97;`npm run test:ui-system` 与 `npm run check:ui-system` 完整链路通过。 +- Electron UI Apps:24/24;主聊天:24 actions / 25 VCP requests;辅助 crash recovery:24 actions / 27 VCP requests。 +- Windows 当前主机最终矩阵:`artifacts/windows-matrix/2026-08-21T06-13-58-840Z.json`,六行全部通过。Settings resize CDP 能力显式 skipped,不计为通过。 +- 当前工作树复核矩阵:`artifacts/windows-matrix/2026-08-21T18-36-55-200Z.json`,六行全部通过(UI Apps、Settings、Next tab、主聊天、辅助 crash recovery、60-cycle lifecycle);该证据仅覆盖当前 Windows 主机与当前 Node/Electron 配置,不替代其他 Windows/打包/GPU-DPI 组合。 +- 历史最终矩阵包含 3 warmup + 60 measured。当前精确补丁(包括 Electron fixture 协议超时与进程树清理修正)的最新复跑为 3 warmup + 20 measured:connected elements 2569、listeners 876、lifecycle resources 163 保持稳定;detached roots/icons/options 均为 0。不得把历史 60-cycle 记录描述为本补丁的重跑证据。 +- 矩阵 `artifacts/windows-matrix/2026-08-21T05-51-24-115Z.json` 与较早的 `2026-08-21T05-10-03-018Z.json` 在主聊天行发生 Puppeteer `Runtime.callFunctionOn` protocol timeout,没有产品断言或失败快照;同一最终代码的独立序列和随后完整串行矩阵通过。失败 artifact 保留为当前主机测试基础设施波动证据,不用于证明产品 PASS。 +- 本次复核也观察到并发或协议超时后 Electron fixture 可能遗留测试进程树。主聊天与 lifecycle harness 现使用启动 PID 的 Windows `taskkill /T /F` 精确清理,并配置独立 protocol timeout;清理验证后只保留复核前已存在的用户 Electron 进程。后续协议超时仍记为失败证据,不通过无限重试升级为产品 PASS。 +- 人工 soak 入口为 `npm run test:manual-soak`;产物状态固定为 `manual_observation_required`,短时 smoke 不能当作人工通过。 +- 当前已完成 30 分钟观察入口:`artifacts/manual-soak/2026-08-21T18-43-55-291Z.json`,30 个每分钟采样、renderer 侧 errors 为空、detached roots 为 0、生命周期资源稳定;artifact 仍是 `manual_observation_required`,且 checklist 尚未由操作员填写,stderr 记录了受控 bad-port 模型获取失败和 degraded 配置提示,需人工判定是否属于环境预期,不能当作产品 PASS。 +- 当前临时 Electron unpacked package 构建未通过:`artifacts/packaged/2026-08-21T19-21-58-000Z.json` 记录 `electron-edge-js` native rebuild 的 MSBuild exit 1;未生成 packaged artifact,因此 packaged smoke 保持 `failed`,不能用 source/runtime smoke 替代。 + +## 文档权威与归档 + +- 当前产品/UI 拓扑只在 `next-ui-current-state.md` 维护。 +- 当前 D0-D7 合同只在 `chat-kernel-deep-decoupling-roadmap.md` 维护;当前阶段和测试数字只在本文维护。 +- C0-C7 路线、D0-D7 时间线审查日志、旧 Web Awesome 并行 presentation 路线和已停止的整页迁移计划已移入 `docs/archive/2026-08-chat-kernel-and-ui-roadmaps/`。归档只用于追溯,不得覆盖当前状态。 + +## 证据工程增量 + +E0-E7 证据自动化框架、10 项契约登记、生成 graph、invalid runner、built-artifact smoke、packaged smoke、四类 stream transcript snapshot、CI manifest 和发布级 evidence gate 见 [`chat-kernel-evidence-and-contracts-roadmap.md`](./chat-kernel-evidence-and-contracts-roadmap.md)。自动 gate、UI Apps 24/24 和主聊天 24-action 单轮 sequence 均已通过;packaged smoke 在没有外部 unpacked 根目录时明确为 `skipped`,发布级 gate 因缺少真实 packaged/Windows/manual 证据不会通过;历史重跑仍出现 CDP protocol timeout,因此真实 Electron 项仍保持 `manual_required`,不能外推为 D7 完成。 + +## 未完成项与完成所需证据 + +1. 在要求支持的 Windows 版本、打包安装方式、GPU/DPI 组合上运行同一矩阵,并分别保存环境、命令、退出码和 stderr。 +2. 运行 30-60 分钟人工 soak;操作员逐项验证发送/流式/取消/重试、历史切换、附件、主题与设置、通知/desktop push、VoiceChat、Rust Assistant、reload/crash、Classic 页面和插件协议,并填写 artifact checklist。 +3. 完成人工 soak 与跨配置矩阵后,重新运行静态门禁和真实 Electron 入口,再进行 vD7 逐项审计。 + +在上述证据齐全前,不得调用整体目标完成标记,也不得在发布说明中宣称 vD7 或最终深度解耦完成。 diff --git a/docs/contracts/README.md b/docs/contracts/README.md new file mode 100644 index 000000000..52433360d --- /dev/null +++ b/docs/contracts/README.md @@ -0,0 +1,9 @@ +# Chat contract evidence + +本目录保存聊天内核证据契约和生成报告。契约 `id` 必须稳定;源码事实由脚本生成,人工决策只记录 owner、动态入口、退役条件和证据状态。 + +- `chat-contract.schema.json`:E0 证据记录的机器校验 schema。 +- `chat-contracts.json`:人工维护的公共契约登记(不存在时允许为空)。 +- `generated/chat-event-graph.json`:由 `build-chat-event-graph.mjs` 生成,不手工编辑。 + +运行 `npm run check:chat-contracts` 校验 schema、生成 graph 并检查 generated 文件没有过期。动态事件入口必须在契约的 `dynamicSites` 中登记;登记结果写入 `registeredDynamic`,未登记入口写入 `undiscovered` 并使 gate 失败,不能用无关 allowlist 隐藏。 diff --git a/docs/contracts/chat-contract.schema.json b/docs/contracts/chat-contract.schema.json new file mode 100644 index 000000000..fda7f0abd --- /dev/null +++ b/docs/contracts/chat-contract.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "vcpchat://contracts/chat-contract.schema.json", + "title": "VCPChat evidence contract", + "type": "object", + "required": ["id", "kind", "owner", "status"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "kind": { "enum": ["event", "capability", "facade", "invariant", "snapshot", "artifact-smoke"] }, + "owner": { "type": "string", "minLength": 1 }, + "status": { "enum": ["pass", "fail", "skipped", "manual_required", "not_applicable"] }, + "producer": { "type": "array", "items": { "type": "string" } }, + "consumers": { "type": "array", "items": { "type": "string" } }, + "mode": { "enum": ["emit", "waterfall", "serial", "parallel", "command"] }, + "durable": { "type": "boolean" }, + "terminal": { "type": "object" }, + "sourceEntry": { "type": "string" }, + "artifactEntry": { "type": "string" }, + "invalidCase": { "type": "string" }, + "snapshot": { "type": "string" }, + "evidence": { "type": "array", "items": { "type": "string" } }, + "retirement": { "type": "string" } + ,"dynamicSmoke": { "type": "string" } + ,"facadeName": { "type": "string" } + ,"dynamicSites": { "type": "array", "items": { "type": "object", "required": ["file", "line"], "properties": { "file": { "type": "string" }, "line": { "type": "integer", "minimum": 1 } }, "additionalProperties": false } } + }, + "additionalProperties": true +} diff --git a/docs/contracts/chat-contracts.json b/docs/contracts/chat-contracts.json new file mode 100644 index 000000000..b4c33d3d1 --- /dev/null +++ b/docs/contracts/chat-contracts.json @@ -0,0 +1,113 @@ +[ + { + "id": "facade.vcp-main-chat-state", + "kind": "facade", + "owner": "renderer.js", + "facadeName": "VCPMainChatState", + "status": "pass", + "consumers": ["Flowlockmodules/flowlock-integration.js", "modules/VChatAutoTTS/plugin.js"], + "dynamicSmoke": "scripts/test-electron-main-chat-sequences.mjs", + "retirement": "retain while Flowlock and AutoTTS plugin consumers use the main-chat state protocol", + "evidence": ["npm run guard:chat-kernel-consumers", "npm run test:electron-main-chat-sequences"] + }, + { + "id": "facade.main-chat-commands", + "kind": "facade", + "owner": "modules/mainChatCommands.js", + "facadeName": "MainChatCommands", + "status": "pass", + "consumers": ["modules/ui-system/next-shell/next-shell-controller.js", "modules/event-listeners.js"], + "dynamicSmoke": "scripts/test-electron-ui-apps-smoke.mjs", + "retirement": "retain while Next shell, Classic menus or plugin commands consume it", + "evidence": ["npm run guard:chat-kernel-consumers", "npm run test:electron-ui-apps"] + }, + { + "id": "facade.vcp-appearance-studio", + "kind": "facade", + "owner": "modules/ui-system/appearance-studio.js", + "facadeName": "VCPAppearanceStudio", + "status": "pass", + "consumers": ["modules/ui-system/next-shell/next-shell-controller.js", "modules/settingsManager.js"], + "dynamicSmoke": "scripts/test-appearance-studio.mjs", + "retirement": "retain while Next shell and settings entrypoints consume it", + "evidence": ["npm run guard:chat-kernel-consumers", "npm run test:appearance-studio"] + }, + { + "id": "chat.stream.lifecycle", + "kind": "event", + "owner": "modules/chat/streamSession.js", + "status": "pass", + "producer": ["modules/chat/streamSession.js", "modules/chat/streamCoordinator.js"], + "consumers": ["modules/renderer/streamManager.js", "modules/chat/vcpStreamBridge.js"], + "mode": "emit", + "durable": false, + "terminal": { "kinds": ["completed", "failed", "cancelled", "discarded"], "single": true, "late": "discard" }, + "sourceEntry": "tests/stream-session.test.mjs", + "invalidCase": "scripts/fixtures/chat-contract-invalid.mjs", + "evidence": ["npm run test:chat-kernel", "npm run test:chat-contract-invariants"] + }, + { + "id": "chat.stream.completed.persistence", + "kind": "event", + "owner": "modules/chat/streamCoordinator.js", + "status": "pass", + "producer": ["modules/chat/streamCoordinator.js"], + "consumers": ["modules/chat/chatHistoryPersistence.js"], + "mode": "emit", + "durable": true, + "terminal": { "kinds": ["completed"], "single": true }, + "sourceEntry": "tests/stream-coordinator.test.mjs", + "evidence": ["npm run test:chat-kernel"] + }, + { + "id": "ui.webawesome.dynamic-event-adapter", + "kind": "event", + "owner": "modules/ui-system/webawesome-adapter.js", + "status": "pass", + "dynamic": true, + "dynamicSites": [{ "file": "modules/ui-system/webawesome-adapter.js", "line": 150 }], + "producer": ["modules/ui-system/webawesome-adapter.js"], + "consumers": ["modules/ui-system/webawesome-adapter.js"], + "mode": "emit", + "durable": false, + "terminal": { "single": false, "late": "surface-scoped" }, + "sourceEntry": "scripts/test-webawesome-adapter.mjs", + "evidence": ["npm run test:ui-system"] + }, + { + "id": "snapshot.chat-stream.completed", + "kind": "snapshot", + "owner": "scripts/test-chat-transcript-snapshot.mjs", + "status": "pass", + "snapshot": "docs/contracts/snapshots/chat-stream.json", + "sourceEntry": "scripts/test-chat-transcript-snapshot.mjs", + "evidence": ["npm run test:chat-transcript-snapshot"] + }, + { + "id": "snapshot.chat-stream.cancelled", + "kind": "snapshot", + "owner": "scripts/test-chat-transcript-snapshot.mjs", + "status": "pass", + "snapshot": "docs/contracts/snapshots/chat-stream-cancel.json", + "sourceEntry": "scripts/test-chat-transcript-snapshot.mjs", + "evidence": ["npm run test:chat-transcript-snapshot"] + }, + { + "id": "snapshot.chat-stream.failed", + "kind": "snapshot", + "owner": "scripts/test-chat-transcript-snapshot.mjs", + "status": "pass", + "snapshot": "docs/contracts/snapshots/chat-stream-failed.json", + "sourceEntry": "scripts/test-chat-transcript-snapshot.mjs", + "evidence": ["npm run test:chat-transcript-snapshot"] + }, + { + "id": "snapshot.chat-stream.discarded", + "kind": "snapshot", + "owner": "scripts/test-chat-transcript-snapshot.mjs", + "status": "pass", + "snapshot": "docs/contracts/snapshots/chat-stream-discarded.json", + "sourceEntry": "scripts/test-chat-transcript-snapshot.mjs", + "evidence": ["npm run test:chat-transcript-snapshot"] + } +] diff --git a/docs/contracts/generated/chat-event-graph.json b/docs/contracts/generated/chat-event-graph.json new file mode 100644 index 000000000..2f327d1a2 --- /dev/null +++ b/docs/contracts/generated/chat-event-graph.json @@ -0,0 +1,9876 @@ +{ + "schemaVersion": 1, + "sourceRoots": [ + "main.js", + "renderer.js", + "preloads", + "modules", + "Flowlockmodules", + "VCPDistributedServer" + ], + "filesScanned": [ + "Flowlockmodules/flowlock-integration.js", + "Flowlockmodules/flowlock-protocol.js", + "Flowlockmodules/flowlock.js", + "VCPDistributedServer/Plugin.js", + "VCPDistributedServer/Plugin/ChatRoomViewer/ChatRoomViewer.js", + "VCPDistributedServer/Plugin/DeepMemo/DeepMemo.js", + "VCPDistributedServer/Plugin/DeepMemo/DeepMemoService.js", + "VCPDistributedServer/Plugin/DesktopRemote/desktop-remote.js", + "VCPDistributedServer/Plugin/DistImageServer/image-server.js", + "VCPDistributedServer/Plugin/FileOperator/CodeValidator.js", + "VCPDistributedServer/Plugin/FileOperator/FileOperator.js", + "VCPDistributedServer/Plugin/LoomController/LoomControllerService.js", + "VCPDistributedServer/Plugin/MusicController/music-controller.js", + "VCPDistributedServer/Plugin/OldPowerShellExecutor/PowerShellExecutor.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/PluginErrorReporter.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.js", + "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "VCPDistributedServer/Plugin/PluginSourceViewer/PluginSourceViewer.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "VCPDistributedServer/Plugin/PowerShellExecutor/test_security_check.js", + "VCPDistributedServer/Plugin/PromptSponsor/prompt-sponsor.js", + "VCPDistributedServer/Plugin/ScriptoriumCollaborator/ScriptoriumCollaboratorService.js", + "VCPDistributedServer/Plugin/TopicMemo/TopicMemo.js", + "VCPDistributedServer/Plugin/TopicSponsor/topicsponsor.js", + "VCPDistributedServer/Plugin/VCPEverything/local-search-controller.js", + "VCPDistributedServer/Plugin/VCPMobileSync/config/defaults.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/db.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/hash.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/idempotency.js", + "VCPDistributedServer/Plugin/VCPMobileSync/core/logger.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/agent.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/group.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/index.js", + "VCPDistributedServer/Plugin/VCPMobileSync/dto/topic.dto.js", + "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "VCPDistributedServer/Plugin/VCPMobileSync/index.js", + "VCPDistributedServer/Plugin/VCPMobileSync/protocol.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/canonical.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/central.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "VCPDistributedServer/Plugin/VCPMobileSync/sync/projection.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/ndjson.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/routes.js", + "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "VCPDistributedServer/Plugin/VCPMobileSync/utils/lock.js", + "VCPDistributedServer/Plugin/VCPMobileSync/utils/mime.js", + "VCPDistributedServer/Plugin/VCPSuperDice/example.js", + "VCPDistributedServer/Plugin/VCPSuperDice/superdice.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/inventory.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/locate.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/media-server.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/lib/we-api-shim.js", + "VCPDistributedServer/Plugin/VCPWEWallpaper/we-wallpaper-service.js", + "VCPDistributedServer/Plugin/VCPWEWallpaperUI/plugin.js", + "VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js", + "VCPDistributedServer/Plugin/VChatDynamicWallpaper/plugin.js", + "VCPDistributedServer/Plugin/WindowSensor/sensor-wrapper.js", + "VCPDistributedServer/VCPDistributedServer.js", + "VCPDistributedServer/frontend-plugin-loader.js", + "main.js", + "modules/SovitsTTS.js", + "modules/assistant/assistant-rust-adapter.js", + "modules/chat/chatContext.js", + "modules/chat/chatDomRenderer.js", + "modules/chat/chatEventContract.js", + "modules/chat/chatHistoryMutationAuthority.js", + "modules/chat/chatHistoryPersistence.js", + "modules/chat/chatOperation.js", + "modules/chat/chatPluginManifest.js", + "modules/chat/chatPresentationSkin.js", + "modules/chat/chatPresentationState.js", + "modules/chat/chatRepository.js", + "modules/chat/chatSurface.js", + "modules/chat/chatSurfaceSlots.js", + "modules/chat/chatThemePlugin.js", + "modules/chat/contentModes.js", + "modules/chat/contentRuntime.js", + "modules/chat/contentTransforms.js", + "modules/chat/mainChatStateAuthority.js", + "modules/chat/memoryChatRepository.js", + "modules/chat/streamConsumerRegistry.js", + "modules/chat/streamCoordinator.js", + "modules/chat/streamSession.js", + "modules/chat/streamTransientHistory.js", + "modules/chat/surfaceConversation.js", + "modules/chat/vcpStreamBridge.js", + "modules/chatManager.js", + "modules/contextSanitizer.js", + "modules/emoticonManager.js", + "modules/event-listeners.js", + "modules/fileManager.js", + "modules/filterManager.js", + "modules/global-settings-manager.js", + "modules/image-viewer.js", + "modules/inputEnhancer.js", + "modules/interruptHandler.js", + "modules/ipc/agentHandlers.js", + "modules/ipc/assistantHandlers.js", + "modules/ipc/canvasHandlers.js", + "modules/ipc/chatHandlers.js", + "modules/ipc/deepWikiHandlers.js", + "modules/ipc/desktopHandlers.js", + "modules/ipc/desktopMetrics.js", + "modules/ipc/desktopRemoteHandlers.js", + "modules/ipc/diceHandlers.js", + "modules/ipc/docxHandlers.js", + "modules/ipc/emoticonHandlers.js", + "modules/ipc/fileDialogHandlers.js", + "modules/ipc/forumHandlers.js", + "modules/ipc/groupChatHandlers.js", + "modules/ipc/ipcContracts.js", + "modules/ipc/libreHardwareMonitorBridge.js", + "modules/ipc/memoHandlers.js", + "modules/ipc/musicHandlers.js", + "modules/ipc/notesHandlers.js", + "modules/ipc/promptHandlers.js", + "modules/ipc/ragHandlers.js", + "modules/ipc/regexHandlers.js", + "modules/ipc/settingsHandlers.js", + "modules/ipc/sovitsHandlers.js", + "modules/ipc/tavernHandlers.js", + "modules/ipc/themeHandlers.js", + "modules/ipc/translatorHandlers.js", + "modules/ipc/voiceHandlers.js", + "modules/ipc/windowHandlers.js", + "modules/itemListManager.js", + "modules/loom/VCPLoomManager.js", + "modules/loom/webcore/adapter-contract.js", + "modules/loom/webcore/chrome-adapter.js", + "modules/loom/webcore/electron-adapter.js", + "modules/loom/webcore/index.js", + "modules/loom/webcore/web-agent-page-core.js", + "modules/loom/webcore/web-agent-page-runtime-core.js", + "modules/loom/webcore/web-agent-protocol.js", + "modules/loom/webcore/web-agent-runtime-core.js", + "modules/lyricFetcher.js", + "modules/mainChatCommands.js", + "modules/messageRenderer.js", + "modules/modelUsageTracker.js", + "modules/musicScannerWorker.js", + "modules/notificationRenderer.js", + "modules/renderer/animation.js", + "modules/renderer/colorUtils.js", + "modules/renderer/contentPipeline.js", + "modules/renderer/contentProcessor.js", + "modules/renderer/desktopPushConsumer.js", + "modules/renderer/domBuilder.js", + "modules/renderer/domListenerOwner.js", + "modules/renderer/emoticonUrlFixer.js", + "modules/renderer/enhancedColorUtils.js", + "modules/renderer/forwardMessageOwner.js", + "modules/renderer/imageHandler.js", + "modules/renderer/mainChatAttachmentOwner.js", + "modules/renderer/mainChatAuxiliaryEventOwner.js", + "modules/renderer/mainChatComposition.js", + "modules/renderer/mainChatDomBindings.js", + "modules/renderer/mainChatEventBridge.js", + "modules/renderer/mainChatFlowlockOwner.js", + "modules/renderer/mainChatSendOwner.js", + "modules/renderer/mainChatSettingsOwner.js", + "modules/renderer/mainChatSettingsPresentationOwner.js", + "modules/renderer/mainChatStreamConsumer.js", + "modules/renderer/mainChatSurfaceAdapter.js", + "modules/renderer/mainChatThemeOwner.js", + "modules/renderer/messageContextMenu.js", + "modules/renderer/middleClickHandler.js", + "modules/renderer/nonStreamingEventConsumer.js", + "modules/renderer/ownedPreloadSubscription.js", + "modules/renderer/pretext-bridge.js", + "modules/renderer/pretext.bundle.js", + "modules/renderer/pretext.esm.js", + "modules/renderer/renderDependencies.js", + "modules/renderer/renderSessionAuthority.js", + "modules/renderer/streamManager.js", + "modules/renderer/streamProjectionRuntime.js", + "modules/renderer/surfaceTaskOwner.js", + "modules/renderer/toolRequestScanner.js", + "modules/renderer/topicSelectionReadiness.js", + "modules/renderer/ttsSurfaceOwner.js", + "modules/renderer/visibilityOptimizer.js", + "modules/renderer/windowStreamRuntime.js", + "modules/searchManager.js", + "modules/services/chatDataService/client.js", + "modules/services/chatDataService/index.js", + "modules/services/chatDataService/lifecycle.js", + "modules/services/deepWikiService.js", + "modules/services/embeddedAppSessionManager.js", + "modules/services/historyWatcherLeaseManager.js", + "modules/services/networkNotesCacheStore.js", + "modules/services/preloadPaths.js", + "modules/services/scriptoriumAgentControlService.js", + "modules/services/scriptoriumFontCacheService.js", + "modules/services/scriptoriumImportService.js", + "modules/services/scriptoriumPptxImportService.js", + "modules/services/senderTaskRegistry.js", + "modules/services/windowAppIds.js", + "modules/services/windowService.js", + "modules/services/windowStateService.js", + "modules/settingsManager.js", + "modules/shared/embeddedAppAllowlist.js", + "modules/speechRecognizer.js", + "modules/tavernRulesEngine.js", + "modules/text-viewer.js", + "modules/topTabManager.js", + "modules/topicListManager.js", + "modules/topicSummarizer.js", + "modules/trayManager.js", + "modules/ui-helpers.js", + "modules/ui-system/appearance-engine.js", + "modules/ui-system/appearance-studio.js", + "modules/ui-system/ask-nova-modal.js", + "modules/ui-system/avatar-picker.js", + "modules/ui-system/component-manifest.js", + "modules/ui-system/component-showcase.js", + "modules/ui-system/contribution-registry.js", + "modules/ui-system/interactive-chat-app.js", + "modules/ui-system/lifecycle-inspector.js", + "modules/ui-system/lifecycle-scope.js", + "modules/ui-system/lucide-adapter.js", + "modules/ui-system/next-shell/account-menu-controller.js", + "modules/ui-system/next-shell/app-tab-host.js", + "modules/ui-system/next-shell/assistant-search-controller.js", + "modules/ui-system/next-shell/creation-controller.js", + "modules/ui-system/next-shell/embedded-app-controller.js", + "modules/ui-system/next-shell/escape-dispatcher.js", + "modules/ui-system/next-shell/launchpad-controller.js", + "modules/ui-system/next-shell/next-shell-controller.js", + "modules/ui-system/next-shell/notification-menu-controller.js", + "modules/ui-system/next-shell/overlay-coordinator.js", + "modules/ui-system/next-ui-apps.js", + "modules/ui-system/performance-recorder.js", + "modules/ui-system/settings-bridge.js", + "modules/ui-system/settlement.js", + "modules/ui-system/sidebar-resizer.js", + "modules/ui-system/standalone-chat-app.js", + "modules/ui-system/startup-theme-gate.js", + "modules/ui-system/state-channel.js", + "modules/ui-system/surface-controller.js", + "modules/ui-system/task-handle.js", + "modules/ui-system/ui-surface-policy.js", + "modules/ui-system/vcp-main-ui-runtime.js", + "modules/ui-system/vcp-ui.js", + "modules/ui-system/webawesome-adapter.js", + "modules/ui-system/webawesome-comparison.js", + "modules/ui-system/webawesome-runtime-manifest.js", + "modules/uiManager.js", + "modules/utils/agentConfigManager.js", + "modules/utils/appSettingsManager.js", + "modules/vcpClient.js", + "modules/weatherService.js", + "modules/webdavManager.js", + "preloads/chat.js", + "preloads/desktop.js", + "preloads/docx.js", + "preloads/loom-page.js", + "preloads/loom.js", + "preloads/shared/apiFactory.js", + "preloads/shared/catalog.js", + "preloads/shared/roles.js", + "preloads/utility.js", + "renderer.js" + ], + "events": [ + { + "name": "apply-theme", + "producers": [ + { + "file": "preloads/chat.js", + "line": 294, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 286, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 197, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 332, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 217, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/themeHandlers.js", + "line": 217, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 294, + "kind": "ipc-send", + "match": "send('apply-theme'" + }, + { + "file": "preloads/desktop.js", + "line": 286, + "kind": "ipc-send", + "match": "send('apply-theme'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 197, + "kind": "ipc-send", + "match": "send('apply-theme'" + }, + { + "file": "preloads/utility.js", + "line": 332, + "kind": "ipc-send", + "match": "send('apply-theme'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 217, + "kind": "custom-event-listener", + "match": "on('apply-theme'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 217, + "kind": "ipc-send", + "match": "on('apply-theme'" + } + ] + }, + { + "name": "ask-nova:cancel", + "producers": [ + { + "file": "preloads/chat.js", + "line": 150, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 150, + "kind": "ipc-send", + "match": "invoke('ask-nova:cancel'" + } + ] + }, + { + "name": "assistant-action", + "producers": [ + { + "file": "preloads/chat.js", + "line": 249, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 244, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 155, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 288, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1304, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1304, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 249, + "kind": "ipc-send", + "match": "send('assistant-action'" + }, + { + "file": "preloads/desktop.js", + "line": 244, + "kind": "ipc-send", + "match": "send('assistant-action'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 155, + "kind": "ipc-send", + "match": "send('assistant-action'" + }, + { + "file": "preloads/utility.js", + "line": 288, + "kind": "ipc-send", + "match": "send('assistant-action'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1304, + "kind": "custom-event-listener", + "match": "on('assistant-action'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1304, + "kind": "ipc-send", + "match": "on('assistant-action'" + } + ] + }, + { + "name": "assistant-bar-data", + "producers": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 314, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 314, + "kind": "ipc-send", + "match": "send('assistant-bar-data'" + } + ] + }, + { + "name": "assistant-data", + "producers": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1093, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1121, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1093, + "kind": "ipc-send", + "match": "send('assistant-data'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1121, + "kind": "ipc-send", + "match": "send('assistant-data'" + } + ] + }, + { + "name": "assistant-suspend-listener", + "producers": [ + { + "file": "preloads/chat.js", + "line": 245, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 240, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 151, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 284, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 245, + "kind": "ipc-send", + "match": "invoke('assistant-suspend-listener'" + }, + { + "file": "preloads/desktop.js", + "line": 240, + "kind": "ipc-send", + "match": "invoke('assistant-suspend-listener'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 151, + "kind": "ipc-send", + "match": "invoke('assistant-suspend-listener'" + }, + { + "file": "preloads/utility.js", + "line": 284, + "kind": "ipc-send", + "match": "invoke('assistant-suspend-listener'" + } + ] + }, + { + "name": "canvas-window-closed", + "producers": [ + { + "file": "modules/ipc/canvasHandlers.js", + "line": 201, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/canvasHandlers.js", + "line": 201, + "kind": "ipc-send", + "match": "send('canvas-window-closed'" + } + ] + }, + { + "name": "claim-pending-flowlock-topic", + "producers": [ + { + "file": "preloads/chat.js", + "line": 267, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 267, + "kind": "ipc-send", + "match": "invoke('claim-pending-flowlock-topic'" + } + ] + }, + { + "name": "close-assistant-bar", + "producers": [ + { + "file": "preloads/chat.js", + "line": 250, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 245, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 156, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 289, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1192, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1192, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 250, + "kind": "ipc-send", + "match": "send('close-assistant-bar'" + }, + { + "file": "preloads/desktop.js", + "line": 245, + "kind": "ipc-send", + "match": "send('close-assistant-bar'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 156, + "kind": "ipc-send", + "match": "send('close-assistant-bar'" + }, + { + "file": "preloads/utility.js", + "line": 289, + "kind": "ipc-send", + "match": "send('close-assistant-bar'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1192, + "kind": "custom-event-listener", + "match": "on('close-assistant-bar'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1192, + "kind": "ipc-send", + "match": "on('close-assistant-bar'" + } + ] + }, + { + "name": "close-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 126, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 125, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 52, + "kind": "ipc-send" + }, + { + "file": "preloads/loom.js", + "line": 46, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 34, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 66, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 50, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 253, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 66, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 737, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 66, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 253, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 737, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 126, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "preloads/desktop.js", + "line": 125, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "preloads/docx.js", + "line": 52, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "preloads/loom.js", + "line": 46, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 34, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 66, + "kind": "custom-event-listener", + "match": "on('close-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 66, + "kind": "ipc-send", + "match": "on('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 50, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 253, + "kind": "custom-event-listener", + "match": "on('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 253, + "kind": "ipc-send", + "match": "on('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 66, + "kind": "ipc-send", + "match": "send('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 737, + "kind": "custom-event-listener", + "match": "on('close-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 737, + "kind": "ipc-send", + "match": "on('close-window'" + } + ] + }, + { + "name": "compact-sidebar-open-topics", + "producers": [ + { + "file": "modules/itemListManager.js", + "line": 872, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/uiManager.js", + "line": 366, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/itemListManager.js", + "line": 872, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('compact-sidebar-open-topics'" + }, + { + "file": "modules/uiManager.js", + "line": 366, + "kind": "custom-event-listener", + "match": "addEventListener('compact-sidebar-open-topics'" + } + ] + }, + { + "name": "connect-vcplog", + "producers": [ + { + "file": "main.js", + "line": 1680, + "kind": "ipc-send" + }, + { + "file": "preloads/chat.js", + "line": 238, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 233, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 144, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 277, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "main.js", + "line": 1680, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "main.js", + "line": 1680, + "kind": "custom-event-listener", + "match": "on('connect-vcplog'" + }, + { + "file": "main.js", + "line": 1680, + "kind": "ipc-send", + "match": "on('connect-vcplog'" + }, + { + "file": "preloads/chat.js", + "line": 238, + "kind": "ipc-send", + "match": "send('connect-vcplog'" + }, + { + "file": "preloads/desktop.js", + "line": 233, + "kind": "ipc-send", + "match": "send('connect-vcplog'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 144, + "kind": "ipc-send", + "match": "send('connect-vcplog'" + }, + { + "file": "preloads/utility.js", + "line": 277, + "kind": "ipc-send", + "match": "send('connect-vcplog'" + } + ] + }, + { + "name": "create-new-topic-for-agent", + "producers": [ + { + "file": "preloads/chat.js", + "line": 192, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 188, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 99, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 232, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 192, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-agent'" + }, + { + "file": "preloads/desktop.js", + "line": 188, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-agent'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 99, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-agent'" + }, + { + "file": "preloads/utility.js", + "line": 232, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-agent'" + } + ] + }, + { + "name": "create-new-topic-for-group", + "producers": [ + { + "file": "preloads/chat.js", + "line": 224, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 219, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 130, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 263, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 224, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-group'" + }, + { + "file": "preloads/desktop.js", + "line": 219, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-group'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 130, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-group'" + }, + { + "file": "preloads/utility.js", + "line": 263, + "kind": "ipc-send", + "match": "invoke('create-new-topic-for-group'" + } + ] + }, + { + "name": "create-unlocked-topic", + "producers": [ + { + "file": "main.js", + "line": 960, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 960, + "kind": "ipc-send", + "match": "send('create-unlocked-topic'" + } + ] + }, + { + "name": "delete-group-topic", + "producers": [ + { + "file": "preloads/chat.js", + "line": 225, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 220, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 131, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 264, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 225, + "kind": "ipc-send", + "match": "invoke('delete-group-topic'" + }, + { + "file": "preloads/desktop.js", + "line": 220, + "kind": "ipc-send", + "match": "invoke('delete-group-topic'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 131, + "kind": "ipc-send", + "match": "invoke('delete-group-topic'" + }, + { + "file": "preloads/utility.js", + "line": 264, + "kind": "ipc-send", + "match": "invoke('delete-group-topic'" + } + ] + }, + { + "name": "delete-topic", + "producers": [ + { + "file": "preloads/chat.js", + "line": 194, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 190, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 101, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 234, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 194, + "kind": "ipc-send", + "match": "invoke('delete-topic'" + }, + { + "file": "preloads/desktop.js", + "line": 190, + "kind": "ipc-send", + "match": "invoke('delete-topic'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 101, + "kind": "ipc-send", + "match": "invoke('delete-topic'" + }, + { + "file": "preloads/utility.js", + "line": 234, + "kind": "ipc-send", + "match": "invoke('delete-topic'" + } + ] + }, + { + "name": "desktop-capture-widget", + "producers": [ + { + "file": "preloads/chat.js", + "line": 408, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 399, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 311, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 446, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 408, + "kind": "ipc-send", + "match": "invoke('desktop-capture-widget'" + }, + { + "file": "preloads/desktop.js", + "line": 399, + "kind": "ipc-send", + "match": "invoke('desktop-capture-widget'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 311, + "kind": "ipc-send", + "match": "invoke('desktop-capture-widget'" + }, + { + "file": "preloads/utility.js", + "line": 446, + "kind": "ipc-send", + "match": "invoke('desktop-capture-widget'" + } + ] + }, + { + "name": "desktop-delete-widget", + "producers": [ + { + "file": "preloads/chat.js", + "line": 403, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 394, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 306, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 441, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 403, + "kind": "ipc-send", + "match": "invoke('desktop-delete-widget'" + }, + { + "file": "preloads/desktop.js", + "line": 394, + "kind": "ipc-send", + "match": "invoke('desktop-delete-widget'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 306, + "kind": "ipc-send", + "match": "invoke('desktop-delete-widget'" + }, + { + "file": "preloads/utility.js", + "line": 441, + "kind": "ipc-send", + "match": "invoke('desktop-delete-widget'" + } + ] + }, + { + "name": "desktop-get-credentials", + "producers": [ + { + "file": "preloads/chat.js", + "line": 409, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 400, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 312, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 447, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 409, + "kind": "ipc-send", + "match": "invoke('desktop-get-credentials'" + }, + { + "file": "preloads/desktop.js", + "line": 400, + "kind": "ipc-send", + "match": "invoke('desktop-get-credentials'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 312, + "kind": "ipc-send", + "match": "invoke('desktop-get-credentials'" + }, + { + "file": "preloads/utility.js", + "line": 447, + "kind": "ipc-send", + "match": "invoke('desktop-get-credentials'" + } + ] + }, + { + "name": "desktop-iconset-get-icon-data", + "producers": [ + { + "file": "preloads/chat.js", + "line": 420, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 412, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 323, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 458, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 420, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-get-icon-data'" + }, + { + "file": "preloads/desktop.js", + "line": 412, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-get-icon-data'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 323, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-get-icon-data'" + }, + { + "file": "preloads/utility.js", + "line": 458, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-get-icon-data'" + } + ] + }, + { + "name": "desktop-iconset-list-icons", + "producers": [ + { + "file": "preloads/chat.js", + "line": 419, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 411, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 322, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 457, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 419, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-icons'" + }, + { + "file": "preloads/desktop.js", + "line": 411, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-icons'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 322, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-icons'" + }, + { + "file": "preloads/utility.js", + "line": 457, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-icons'" + } + ] + }, + { + "name": "desktop-iconset-list-presets", + "producers": [ + { + "file": "preloads/chat.js", + "line": 418, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 410, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 321, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 456, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 418, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-presets'" + }, + { + "file": "preloads/desktop.js", + "line": 410, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-presets'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 321, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-presets'" + }, + { + "file": "preloads/utility.js", + "line": 456, + "kind": "ipc-send", + "match": "invoke('desktop-iconset-list-presets'" + } + ] + }, + { + "name": "desktop-launch-vchat-app", + "producers": [ + { + "file": "preloads/chat.js", + "line": 421, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 413, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 324, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 459, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 421, + "kind": "ipc-send", + "match": "invoke('desktop-launch-vchat-app'" + }, + { + "file": "preloads/desktop.js", + "line": 413, + "kind": "ipc-send", + "match": "invoke('desktop-launch-vchat-app'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 324, + "kind": "ipc-send", + "match": "invoke('desktop-launch-vchat-app'" + }, + { + "file": "preloads/utility.js", + "line": 459, + "kind": "ipc-send", + "match": "invoke('desktop-launch-vchat-app'" + } + ] + }, + { + "name": "desktop-list-widget-files", + "producers": [ + { + "file": "preloads/chat.js", + "line": 407, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 398, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 310, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 445, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 407, + "kind": "ipc-send", + "match": "invoke('desktop-list-widget-files'" + }, + { + "file": "preloads/desktop.js", + "line": 398, + "kind": "ipc-send", + "match": "invoke('desktop-list-widget-files'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 310, + "kind": "ipc-send", + "match": "invoke('desktop-list-widget-files'" + }, + { + "file": "preloads/utility.js", + "line": 445, + "kind": "ipc-send", + "match": "invoke('desktop-list-widget-files'" + } + ] + }, + { + "name": "desktop-list-widgets", + "producers": [ + { + "file": "preloads/chat.js", + "line": 404, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 395, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 307, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 442, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 404, + "kind": "ipc-send", + "match": "invoke('desktop-list-widgets'" + }, + { + "file": "preloads/desktop.js", + "line": 395, + "kind": "ipc-send", + "match": "invoke('desktop-list-widgets'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 307, + "kind": "ipc-send", + "match": "invoke('desktop-list-widgets'" + }, + { + "file": "preloads/utility.js", + "line": 442, + "kind": "ipc-send", + "match": "invoke('desktop-list-widgets'" + } + ] + }, + { + "name": "desktop-load-dock", + "producers": [ + { + "file": "preloads/chat.js", + "line": 415, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 406, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 318, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 453, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 415, + "kind": "ipc-send", + "match": "invoke('desktop-load-dock'" + }, + { + "file": "preloads/desktop.js", + "line": 406, + "kind": "ipc-send", + "match": "invoke('desktop-load-dock'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 318, + "kind": "ipc-send", + "match": "invoke('desktop-load-dock'" + }, + { + "file": "preloads/utility.js", + "line": 453, + "kind": "ipc-send", + "match": "invoke('desktop-load-dock'" + } + ] + }, + { + "name": "desktop-load-layout", + "producers": [ + { + "file": "preloads/chat.js", + "line": 417, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 409, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 320, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 455, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 417, + "kind": "ipc-send", + "match": "invoke('desktop-load-layout'" + }, + { + "file": "preloads/desktop.js", + "line": 409, + "kind": "ipc-send", + "match": "invoke('desktop-load-layout'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 320, + "kind": "ipc-send", + "match": "invoke('desktop-load-layout'" + }, + { + "file": "preloads/utility.js", + "line": 455, + "kind": "ipc-send", + "match": "invoke('desktop-load-layout'" + } + ] + }, + { + "name": "desktop-load-widget", + "producers": [ + { + "file": "preloads/chat.js", + "line": 402, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 393, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 305, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 440, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 402, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget'" + }, + { + "file": "preloads/desktop.js", + "line": 393, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 305, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget'" + }, + { + "file": "preloads/utility.js", + "line": 440, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget'" + } + ] + }, + { + "name": "desktop-load-widget-file", + "producers": [ + { + "file": "preloads/chat.js", + "line": 406, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 397, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 309, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 444, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 406, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget-file'" + }, + { + "file": "preloads/desktop.js", + "line": 397, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget-file'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 309, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget-file'" + }, + { + "file": "preloads/utility.js", + "line": 444, + "kind": "ipc-send", + "match": "invoke('desktop-load-widget-file'" + } + ] + }, + { + "name": "desktop-metrics-get-capabilities", + "producers": [ + { + "file": "preloads/chat.js", + "line": 449, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 422, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 344, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 464, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 449, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-capabilities'" + }, + { + "file": "preloads/desktop.js", + "line": 422, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-capabilities'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 344, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-capabilities'" + }, + { + "file": "preloads/utility.js", + "line": 464, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-capabilities'" + } + ] + }, + { + "name": "desktop-metrics-get-detailed-processes", + "producers": [ + { + "file": "preloads/chat.js", + "line": 450, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 423, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 345, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 465, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 450, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-detailed-processes'" + }, + { + "file": "preloads/desktop.js", + "line": 423, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-detailed-processes'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 345, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-detailed-processes'" + }, + { + "file": "preloads/utility.js", + "line": 465, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-detailed-processes'" + } + ] + }, + { + "name": "desktop-metrics-get-snapshot", + "producers": [ + { + "file": "preloads/chat.js", + "line": 448, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 421, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 343, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 463, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 448, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-snapshot'" + }, + { + "file": "preloads/desktop.js", + "line": 421, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-snapshot'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 343, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-snapshot'" + }, + { + "file": "preloads/utility.js", + "line": 463, + "kind": "ipc-send", + "match": "invoke('desktop-metrics-get-snapshot'" + } + ] + }, + { + "name": "desktop-open-system-tool", + "producers": [ + { + "file": "preloads/chat.js", + "line": 451, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 424, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 346, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 466, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 451, + "kind": "ipc-send", + "match": "invoke('desktop-open-system-tool'" + }, + { + "file": "preloads/desktop.js", + "line": 424, + "kind": "ipc-send", + "match": "invoke('desktop-open-system-tool'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 346, + "kind": "ipc-send", + "match": "invoke('desktop-open-system-tool'" + }, + { + "file": "preloads/utility.js", + "line": 466, + "kind": "ipc-send", + "match": "invoke('desktop-open-system-tool'" + } + ] + }, + { + "name": "desktop-open-widget-in-canvas", + "producers": [ + { + "file": "preloads/desktop.js", + "line": 425, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 467, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/desktop.js", + "line": 425, + "kind": "ipc-send", + "match": "invoke('desktop-open-widget-in-canvas'" + }, + { + "file": "preloads/utility.js", + "line": 467, + "kind": "ipc-send", + "match": "invoke('desktop-open-widget-in-canvas'" + } + ] + }, + { + "name": "desktop-patch-layout", + "producers": [ + { + "file": "preloads/desktop.js", + "line": 408, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/desktop.js", + "line": 408, + "kind": "ipc-send", + "match": "invoke('desktop-patch-layout'" + } + ] + }, + { + "name": "desktop-push", + "producers": [ + { + "file": "preloads/chat.js", + "line": 395, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 386, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 298, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 433, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 998, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/desktopHandlers.js", + "line": 998, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 395, + "kind": "ipc-send", + "match": "send('desktop-push'" + }, + { + "file": "preloads/desktop.js", + "line": 386, + "kind": "ipc-send", + "match": "send('desktop-push'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 298, + "kind": "ipc-send", + "match": "send('desktop-push'" + }, + { + "file": "preloads/utility.js", + "line": 433, + "kind": "ipc-send", + "match": "send('desktop-push'" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 998, + "kind": "custom-event-listener", + "match": "on('desktop-push'" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 998, + "kind": "ipc-send", + "match": "on('desktop-push'" + } + ] + }, + { + "name": "desktop-push-to-canvas", + "producers": [ + { + "file": "modules/ipc/desktopHandlers.js", + "line": 1000, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2643, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/desktopHandlers.js", + "line": 1000, + "kind": "ipc-send", + "match": "send('desktop-push-to-canvas'" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2643, + "kind": "ipc-send", + "match": "send('desktop-push-to-canvas'" + } + ] + }, + { + "name": "desktop-read-wallpaper-thumbnail", + "producers": [ + { + "file": "preloads/chat.js", + "line": 446, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 419, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 341, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 461, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 446, + "kind": "ipc-send", + "match": "invoke('desktop-read-wallpaper-thumbnail'" + }, + { + "file": "preloads/desktop.js", + "line": 419, + "kind": "ipc-send", + "match": "invoke('desktop-read-wallpaper-thumbnail'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 341, + "kind": "ipc-send", + "match": "invoke('desktop-read-wallpaper-thumbnail'" + }, + { + "file": "preloads/utility.js", + "line": 461, + "kind": "ipc-send", + "match": "invoke('desktop-read-wallpaper-thumbnail'" + } + ] + }, + { + "name": "desktop-remote-set-wallpaper", + "producers": [ + { + "file": "modules/ipc/desktopRemoteHandlers.js", + "line": 342, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/desktopRemoteHandlers.js", + "line": 342, + "kind": "ipc-send", + "match": "send('desktop-remote-set-wallpaper'" + } + ] + }, + { + "name": "desktop-remote:response", + "producers": [ + { + "file": "preloads/chat.js", + "line": 400, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 391, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 303, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 438, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 400, + "kind": "ipc-send", + "match": "send('desktop-remote:response'" + }, + { + "file": "preloads/desktop.js", + "line": 391, + "kind": "ipc-send", + "match": "send('desktop-remote:response'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 303, + "kind": "ipc-send", + "match": "send('desktop-remote:response'" + }, + { + "file": "preloads/utility.js", + "line": 438, + "kind": "ipc-send", + "match": "send('desktop-remote:response'" + } + ] + }, + { + "name": "desktop-save-dock", + "producers": [ + { + "file": "preloads/chat.js", + "line": 414, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 405, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 317, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 452, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 414, + "kind": "ipc-send", + "match": "invoke('desktop-save-dock'" + }, + { + "file": "preloads/desktop.js", + "line": 405, + "kind": "ipc-send", + "match": "invoke('desktop-save-dock'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 317, + "kind": "ipc-send", + "match": "invoke('desktop-save-dock'" + }, + { + "file": "preloads/utility.js", + "line": 452, + "kind": "ipc-send", + "match": "invoke('desktop-save-dock'" + } + ] + }, + { + "name": "desktop-save-layout", + "producers": [ + { + "file": "preloads/chat.js", + "line": 416, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 407, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 319, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 454, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 416, + "kind": "ipc-send", + "match": "invoke('desktop-save-layout'" + }, + { + "file": "preloads/desktop.js", + "line": 407, + "kind": "ipc-send", + "match": "invoke('desktop-save-layout'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 319, + "kind": "ipc-send", + "match": "invoke('desktop-save-layout'" + }, + { + "file": "preloads/utility.js", + "line": 454, + "kind": "ipc-send", + "match": "invoke('desktop-save-layout'" + } + ] + }, + { + "name": "desktop-save-widget", + "producers": [ + { + "file": "preloads/chat.js", + "line": 401, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 392, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 304, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 439, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 401, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget'" + }, + { + "file": "preloads/desktop.js", + "line": 392, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 304, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget'" + }, + { + "file": "preloads/utility.js", + "line": 439, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget'" + } + ] + }, + { + "name": "desktop-save-widget-file", + "producers": [ + { + "file": "preloads/chat.js", + "line": 405, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 396, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 308, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 443, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 405, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget-file'" + }, + { + "file": "preloads/desktop.js", + "line": 396, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget-file'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 308, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget-file'" + }, + { + "file": "preloads/utility.js", + "line": 443, + "kind": "ipc-send", + "match": "invoke('desktop-save-widget-file'" + } + ] + }, + { + "name": "desktop-scan-shortcuts", + "producers": [ + { + "file": "preloads/chat.js", + "line": 413, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 404, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 316, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 451, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 413, + "kind": "ipc-send", + "match": "invoke('desktop-scan-shortcuts'" + }, + { + "file": "preloads/desktop.js", + "line": 404, + "kind": "ipc-send", + "match": "invoke('desktop-scan-shortcuts'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 316, + "kind": "ipc-send", + "match": "invoke('desktop-scan-shortcuts'" + }, + { + "file": "preloads/utility.js", + "line": 451, + "kind": "ipc-send", + "match": "invoke('desktop-scan-shortcuts'" + } + ] + }, + { + "name": "desktop-select-wallpaper", + "producers": [ + { + "file": "preloads/chat.js", + "line": 445, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 418, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 340, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 460, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 445, + "kind": "ipc-send", + "match": "invoke('desktop-select-wallpaper'" + }, + { + "file": "preloads/desktop.js", + "line": 418, + "kind": "ipc-send", + "match": "invoke('desktop-select-wallpaper'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 340, + "kind": "ipc-send", + "match": "invoke('desktop-select-wallpaper'" + }, + { + "file": "preloads/utility.js", + "line": 460, + "kind": "ipc-send", + "match": "invoke('desktop-select-wallpaper'" + } + ] + }, + { + "name": "desktop-set-always-on-bottom", + "producers": [ + { + "file": "preloads/chat.js", + "line": 447, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 420, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 342, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 462, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 447, + "kind": "ipc-send", + "match": "invoke('desktop-set-always-on-bottom'" + }, + { + "file": "preloads/desktop.js", + "line": 420, + "kind": "ipc-send", + "match": "invoke('desktop-set-always-on-bottom'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 342, + "kind": "ipc-send", + "match": "invoke('desktop-set-always-on-bottom'" + }, + { + "file": "preloads/utility.js", + "line": 462, + "kind": "ipc-send", + "match": "invoke('desktop-set-always-on-bottom'" + } + ] + }, + { + "name": "desktop-shortcut-launch", + "producers": [ + { + "file": "preloads/chat.js", + "line": 412, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 403, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 315, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 450, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 412, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-launch'" + }, + { + "file": "preloads/desktop.js", + "line": 403, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-launch'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 315, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-launch'" + }, + { + "file": "preloads/utility.js", + "line": 450, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-launch'" + } + ] + }, + { + "name": "desktop-shortcut-parse", + "producers": [ + { + "file": "preloads/chat.js", + "line": 410, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 401, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 313, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 448, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 410, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse'" + }, + { + "file": "preloads/desktop.js", + "line": 401, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 313, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse'" + }, + { + "file": "preloads/utility.js", + "line": 448, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse'" + } + ] + }, + { + "name": "desktop-shortcut-parse-batch", + "producers": [ + { + "file": "preloads/chat.js", + "line": 411, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 402, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 314, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 449, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 411, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse-batch'" + }, + { + "file": "preloads/desktop.js", + "line": 402, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse-batch'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 314, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse-batch'" + }, + { + "file": "preloads/utility.js", + "line": 449, + "kind": "ipc-send", + "match": "invoke('desktop-shortcut-parse-batch'" + } + ] + }, + { + "name": "desktop-status", + "producers": [ + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2380, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2384, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2425, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2380, + "kind": "ipc-send", + "match": "send('desktop-status'" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2384, + "kind": "ipc-send", + "match": "send('desktop-status'" + }, + { + "file": "modules/ipc/desktopHandlers.js", + "line": 2425, + "kind": "ipc-send", + "match": "send('desktop-status'" + } + ] + }, + { + "name": "desktop-widget-source-saved", + "producers": [ + { + "file": "modules/ipc/canvasHandlers.js", + "line": 83, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/canvasHandlers.js", + "line": 83, + "kind": "ipc-send", + "match": "send('desktop-widget-source-saved'" + } + ] + }, + { + "name": "disconnect-vcplog", + "producers": [ + { + "file": "main.js", + "line": 1691, + "kind": "ipc-send" + }, + { + "file": "preloads/chat.js", + "line": 239, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 234, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 145, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 278, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "main.js", + "line": 1691, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "main.js", + "line": 1691, + "kind": "custom-event-listener", + "match": "on('disconnect-vcplog'" + }, + { + "file": "main.js", + "line": 1691, + "kind": "ipc-send", + "match": "on('disconnect-vcplog'" + }, + { + "file": "preloads/chat.js", + "line": 239, + "kind": "ipc-send", + "match": "send('disconnect-vcplog'" + }, + { + "file": "preloads/desktop.js", + "line": 234, + "kind": "ipc-send", + "match": "send('disconnect-vcplog'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 145, + "kind": "ipc-send", + "match": "send('disconnect-vcplog'" + }, + { + "file": "preloads/utility.js", + "line": 278, + "kind": "ipc-send", + "match": "send('disconnect-vcplog'" + } + ] + }, + { + "name": "do-toggle-notifications-sidebar", + "producers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 109, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 109, + "kind": "ipc-send", + "match": "send('do-toggle-notifications-sidebar'" + } + ] + }, + { + "name": "embedded-vchat-app-state", + "producers": [ + { + "file": "modules/services/embeddedAppSessionManager.js", + "line": 81, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/services/embeddedAppSessionManager.js", + "line": 81, + "kind": "ipc-send", + "match": "send('embedded-vchat-app-state'" + } + ] + }, + { + "name": "embedded-vchat-app:activate", + "producers": [ + { + "file": "preloads/chat.js", + "line": 430, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 430, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:activate'" + } + ] + }, + { + "name": "embedded-vchat-app:cancel", + "producers": [ + { + "file": "preloads/chat.js", + "line": 441, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 338, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 441, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:cancel'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 338, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:cancel'" + } + ] + }, + { + "name": "embedded-vchat-app:close-all", + "producers": [ + { + "file": "preloads/chat.js", + "line": 443, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 443, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:close-all'" + } + ] + }, + { + "name": "embedded-vchat-app:list", + "producers": [ + { + "file": "preloads/chat.js", + "line": 429, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 429, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:list'" + } + ] + }, + { + "name": "embedded-vchat-app:request-close", + "producers": [ + { + "file": "main.js", + "line": 1360, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "main.js", + "line": 1360, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "main.js", + "line": 1360, + "kind": "custom-event-listener", + "match": "on('embedded-vchat-app:request-close'" + }, + { + "file": "main.js", + "line": 1360, + "kind": "ipc-send", + "match": "on('embedded-vchat-app:request-close'" + } + ] + }, + { + "name": "embedded-vchat-app:set-bounds", + "producers": [ + { + "file": "preloads/chat.js", + "line": 431, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 431, + "kind": "ipc-send", + "match": "invoke('embedded-vchat-app:set-bounds'" + } + ] + }, + { + "name": "export-topic-as-markdown", + "producers": [ + { + "file": "preloads/chat.js", + "line": 237, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 232, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 143, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 276, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 237, + "kind": "ipc-send", + "match": "invoke('export-topic-as-markdown'" + }, + { + "file": "preloads/desktop.js", + "line": 232, + "kind": "ipc-send", + "match": "invoke('export-topic-as-markdown'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 143, + "kind": "ipc-send", + "match": "invoke('export-topic-as-markdown'" + }, + { + "file": "preloads/utility.js", + "line": 276, + "kind": "ipc-send", + "match": "invoke('export-topic-as-markdown'" + } + ] + }, + { + "name": "flowlock-command", + "producers": [ + { + "file": "modules/ipc/desktopRemoteHandlers.js", + "line": 203, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/desktopRemoteHandlers.js", + "line": 203, + "kind": "ipc-send", + "match": "send('flowlock-command'" + } + ] + }, + { + "name": "flowlock:response", + "producers": [ + { + "file": "preloads/chat.js", + "line": 266, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 261, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 172, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 305, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 266, + "kind": "ipc-send", + "match": "send('flowlock:response'" + }, + { + "file": "preloads/desktop.js", + "line": 261, + "kind": "ipc-send", + "match": "send('flowlock:response'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 172, + "kind": "ipc-send", + "match": "send('flowlock:response'" + }, + { + "file": "preloads/utility.js", + "line": 305, + "kind": "ipc-send", + "match": "send('flowlock:response'" + } + ] + }, + { + "name": "get-agent-topics", + "producers": [ + { + "file": "preloads/chat.js", + "line": 191, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 187, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 98, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 231, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 191, + "kind": "ipc-send", + "match": "invoke('get-agent-topics'" + }, + { + "file": "preloads/desktop.js", + "line": 187, + "kind": "ipc-send", + "match": "invoke('get-agent-topics'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 98, + "kind": "ipc-send", + "match": "invoke('get-agent-topics'" + }, + { + "file": "preloads/utility.js", + "line": 231, + "kind": "ipc-send", + "match": "invoke('get-agent-topics'" + } + ] + }, + { + "name": "get-assistant-bar-initial-data", + "producers": [ + { + "file": "preloads/chat.js", + "line": 252, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 247, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 158, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 291, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 252, + "kind": "ipc-send", + "match": "invoke('get-assistant-bar-initial-data'" + }, + { + "file": "preloads/desktop.js", + "line": 247, + "kind": "ipc-send", + "match": "invoke('get-assistant-bar-initial-data'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 158, + "kind": "ipc-send", + "match": "invoke('get-assistant-bar-initial-data'" + }, + { + "file": "preloads/utility.js", + "line": 291, + "kind": "ipc-send", + "match": "invoke('get-assistant-bar-initial-data'" + } + ] + }, + { + "name": "get-assistant-runtime-status", + "producers": [ + { + "file": "preloads/chat.js", + "line": 246, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 241, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 152, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 285, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 246, + "kind": "ipc-send", + "match": "invoke('get-assistant-runtime-status'" + }, + { + "file": "preloads/desktop.js", + "line": 241, + "kind": "ipc-send", + "match": "invoke('get-assistant-runtime-status'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 152, + "kind": "ipc-send", + "match": "invoke('get-assistant-runtime-status'" + }, + { + "file": "preloads/utility.js", + "line": 285, + "kind": "ipc-send", + "match": "invoke('get-assistant-runtime-status'" + } + ] + }, + { + "name": "get-chat-history", + "producers": [ + { + "file": "preloads/chat.js", + "line": 199, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 195, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 106, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 239, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 199, + "kind": "ipc-send", + "match": "invoke('get-chat-history'" + }, + { + "file": "preloads/desktop.js", + "line": 195, + "kind": "ipc-send", + "match": "invoke('get-chat-history'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 106, + "kind": "ipc-send", + "match": "invoke('get-chat-history'" + }, + { + "file": "preloads/utility.js", + "line": 239, + "kind": "ipc-send", + "match": "invoke('get-chat-history'" + } + ] + }, + { + "name": "get-current-theme", + "producers": [ + { + "file": "preloads/chat.js", + "line": 144, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 142, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 43, + "kind": "ipc-send" + }, + { + "file": "preloads/loom.js", + "line": 41, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 52, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 183, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 144, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + }, + { + "file": "preloads/desktop.js", + "line": 142, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + }, + { + "file": "preloads/docx.js", + "line": 43, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + }, + { + "file": "preloads/loom.js", + "line": 41, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 52, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + }, + { + "file": "preloads/utility.js", + "line": 183, + "kind": "ipc-send", + "match": "invoke('get-current-theme'" + } + ] + }, + { + "name": "get-group-chat-history", + "producers": [ + { + "file": "preloads/chat.js", + "line": 227, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 222, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 133, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 266, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 227, + "kind": "ipc-send", + "match": "invoke('get-group-chat-history'" + }, + { + "file": "preloads/desktop.js", + "line": 222, + "kind": "ipc-send", + "match": "invoke('get-group-chat-history'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 133, + "kind": "ipc-send", + "match": "invoke('get-group-chat-history'" + }, + { + "file": "preloads/utility.js", + "line": 266, + "kind": "ipc-send", + "match": "invoke('get-group-chat-history'" + } + ] + }, + { + "name": "get-group-topics", + "producers": [ + { + "file": "preloads/chat.js", + "line": 223, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 218, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 129, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 262, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 223, + "kind": "ipc-send", + "match": "invoke('get-group-topics'" + }, + { + "file": "preloads/desktop.js", + "line": 218, + "kind": "ipc-send", + "match": "invoke('get-group-topics'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 129, + "kind": "ipc-send", + "match": "invoke('get-group-topics'" + }, + { + "file": "preloads/utility.js", + "line": 262, + "kind": "ipc-send", + "match": "invoke('get-group-topics'" + } + ] + }, + { + "name": "get-original-message-content", + "producers": [ + { + "file": "preloads/chat.js", + "line": 201, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 197, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 108, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 241, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 201, + "kind": "ipc-send", + "match": "invoke('get-original-message-content'" + }, + { + "file": "preloads/desktop.js", + "line": 197, + "kind": "ipc-send", + "match": "invoke('get-original-message-content'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 108, + "kind": "ipc-send", + "match": "invoke('get-original-message-content'" + }, + { + "file": "preloads/utility.js", + "line": 241, + "kind": "ipc-send", + "match": "invoke('get-original-message-content'" + } + ] + }, + { + "name": "get-rust-assistant-config", + "producers": [ + { + "file": "preloads/chat.js", + "line": 247, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 242, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 153, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 286, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 247, + "kind": "ipc-send", + "match": "invoke('get-rust-assistant-config'" + }, + { + "file": "preloads/desktop.js", + "line": 242, + "kind": "ipc-send", + "match": "invoke('get-rust-assistant-config'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 153, + "kind": "ipc-send", + "match": "invoke('get-rust-assistant-config'" + }, + { + "file": "preloads/utility.js", + "line": 286, + "kind": "ipc-send", + "match": "invoke('get-rust-assistant-config'" + } + ] + }, + { + "name": "get-selection-listener-status", + "producers": [ + { + "file": "preloads/chat.js", + "line": 244, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 239, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 150, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 283, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 244, + "kind": "ipc-send", + "match": "invoke('get-selection-listener-status'" + }, + { + "file": "preloads/desktop.js", + "line": 239, + "kind": "ipc-send", + "match": "invoke('get-selection-listener-status'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 150, + "kind": "ipc-send", + "match": "invoke('get-selection-listener-status'" + }, + { + "file": "preloads/utility.js", + "line": 283, + "kind": "ipc-send", + "match": "invoke('get-selection-listener-status'" + } + ] + }, + { + "name": "get-themes", + "producers": [ + { + "file": "preloads/chat.js", + "line": 293, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 285, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 196, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 331, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 293, + "kind": "ipc-send", + "match": "invoke('get-themes'" + }, + { + "file": "preloads/desktop.js", + "line": 285, + "kind": "ipc-send", + "match": "invoke('get-themes'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 196, + "kind": "ipc-send", + "match": "invoke('get-themes'" + }, + { + "file": "preloads/utility.js", + "line": 331, + "kind": "ipc-send", + "match": "invoke('get-themes'" + } + ] + }, + { + "name": "get-unread-topic-counts", + "producers": [ + { + "file": "preloads/chat.js", + "line": 195, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 191, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 102, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 235, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 195, + "kind": "ipc-send", + "match": "invoke('get-unread-topic-counts'" + }, + { + "file": "preloads/desktop.js", + "line": 191, + "kind": "ipc-send", + "match": "invoke('get-unread-topic-counts'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 102, + "kind": "ipc-send", + "match": "invoke('get-unread-topic-counts'" + }, + { + "file": "preloads/utility.js", + "line": 235, + "kind": "ipc-send", + "match": "invoke('get-unread-topic-counts'" + } + ] + }, + { + "name": "global-settings-updated", + "producers": [ + { + "file": "modules/global-settings-manager.js", + "line": 304, + "kind": "custom-event-dispatch" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 321, + "kind": "custom-event-dispatch" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1169, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/global-settings-manager.js", + "line": 304, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('global-settings-updated'" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 321, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('global-settings-updated'" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1169, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('global-settings-updated'" + } + ] + }, + { + "name": "hide-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 127, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 126, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 35, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 163, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 100, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 100, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 127, + "kind": "ipc-send", + "match": "send('hide-window'" + }, + { + "file": "preloads/desktop.js", + "line": 126, + "kind": "ipc-send", + "match": "send('hide-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 35, + "kind": "ipc-send", + "match": "send('hide-window'" + }, + { + "file": "preloads/utility.js", + "line": 163, + "kind": "ipc-send", + "match": "send('hide-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 100, + "kind": "custom-event-listener", + "match": "on('hide-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 100, + "kind": "ipc-send", + "match": "on('hide-window'" + } + ] + }, + { + "name": "history", + "producers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 171, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 172, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 171, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 172, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 171, + "kind": "custom-event-listener", + "match": "on(\"history\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 172, + "kind": "custom-event-listener", + "match": "on(\"history\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 171, + "kind": "ipc-send", + "match": "on(\"history\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 172, + "kind": "ipc-send", + "match": "on(\"history\"" + } + ] + }, + { + "name": "history-file-updated", + "producers": [ + { + "file": "main.js", + "line": 1224, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 1224, + "kind": "ipc-send", + "match": "send('history-file-updated'" + } + ] + }, + { + "name": "interrupt-vcp-request", + "producers": [ + { + "file": "preloads/chat.js", + "line": 216, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 211, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 122, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 255, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 216, + "kind": "ipc-send", + "match": "invoke('interrupt-vcp-request'" + }, + { + "file": "preloads/desktop.js", + "line": 211, + "kind": "ipc-send", + "match": "invoke('interrupt-vcp-request'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 122, + "kind": "ipc-send", + "match": "invoke('interrupt-vcp-request'" + }, + { + "file": "preloads/utility.js", + "line": 255, + "kind": "ipc-send", + "match": "invoke('interrupt-vcp-request'" + } + ] + }, + { + "name": "list-enabled-frontend-plugins", + "producers": [ + { + "file": "preloads/chat.js", + "line": 423, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 423, + "kind": "ipc-send", + "match": "invoke('list-enabled-frontend-plugins'" + } + ] + }, + { + "name": "list-pending-flowlock-topics", + "producers": [ + { + "file": "preloads/chat.js", + "line": 269, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 269, + "kind": "ipc-send", + "match": "invoke('list-pending-flowlock-topics'" + } + ] + }, + { + "name": "load-settings", + "producers": [ + { + "file": "preloads/chat.js", + "line": 104, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 104, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 10, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 133, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 104, + "kind": "ipc-send", + "match": "invoke('load-settings'" + }, + { + "file": "preloads/desktop.js", + "line": 104, + "kind": "ipc-send", + "match": "invoke('load-settings'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 10, + "kind": "ipc-send", + "match": "invoke('load-settings'" + }, + { + "file": "preloads/utility.js", + "line": 133, + "kind": "ipc-send", + "match": "invoke('load-settings'" + } + ] + }, + { + "name": "load-translator-settings", + "producers": [ + { + "file": "preloads/shared/catalog.js", + "line": 12, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 134, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/shared/catalog.js", + "line": 12, + "kind": "ipc-send", + "match": "invoke('load-translator-settings'" + }, + { + "file": "preloads/utility.js", + "line": 134, + "kind": "ipc-send", + "match": "invoke('load-translator-settings'" + } + ] + }, + { + "name": "maximize-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 124, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 123, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 50, + "kind": "ipc-send" + }, + { + "file": "preloads/loom.js", + "line": 45, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 32, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 154, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 48, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 49, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 243, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 65, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 732, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 48, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 243, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 732, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 124, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "preloads/desktop.js", + "line": 123, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "preloads/docx.js", + "line": 50, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "preloads/loom.js", + "line": 45, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 32, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "preloads/utility.js", + "line": 154, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 48, + "kind": "custom-event-listener", + "match": "on('maximize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 48, + "kind": "ipc-send", + "match": "on('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 49, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 243, + "kind": "custom-event-listener", + "match": "on('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 243, + "kind": "ipc-send", + "match": "on('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 65, + "kind": "ipc-send", + "match": "send('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 732, + "kind": "custom-event-listener", + "match": "on('maximize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 732, + "kind": "ipc-send", + "match": "on('maximize-window'" + } + ] + }, + { + "name": "message", + "producers": [ + { + "file": "modules/ipc/musicHandlers.js", + "line": 612, + "kind": "ipc-send" + }, + { + "file": "modules/loom/webcore/electron-adapter.js", + "line": 384, + "kind": "ipc-send" + }, + { + "file": "modules/musicScannerWorker.js", + "line": 14, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 250, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 286, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/musicHandlers.js", + "line": 612, + "kind": "custom-event-listener" + }, + { + "file": "modules/loom/webcore/electron-adapter.js", + "line": 384, + "kind": "custom-event-listener" + }, + { + "file": "modules/musicScannerWorker.js", + "line": 14, + "kind": "custom-event-listener" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 811, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 250, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 286, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ipc/musicHandlers.js", + "line": 612, + "kind": "custom-event-listener", + "match": "on('message'" + }, + { + "file": "modules/ipc/musicHandlers.js", + "line": 612, + "kind": "ipc-send", + "match": "on('message'" + }, + { + "file": "modules/loom/webcore/electron-adapter.js", + "line": 384, + "kind": "custom-event-listener", + "match": "on('message'" + }, + { + "file": "modules/loom/webcore/electron-adapter.js", + "line": 384, + "kind": "ipc-send", + "match": "on('message'" + }, + { + "file": "modules/musicScannerWorker.js", + "line": 14, + "kind": "custom-event-listener", + "match": "on('message'" + }, + { + "file": "modules/musicScannerWorker.js", + "line": 14, + "kind": "ipc-send", + "match": "on('message'" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 811, + "kind": "custom-event-listener", + "match": "addEventListener('message'" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 250, + "kind": "custom-event-listener", + "match": "on(\"message\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 250, + "kind": "ipc-send", + "match": "on(\"message\"" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 286, + "kind": "custom-event-listener", + "match": "on('message'" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 286, + "kind": "ipc-send", + "match": "on('message'" + } + ] + }, + { + "name": "messages", + "producers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 109, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 128, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 134, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 138, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 139, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 142, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 143, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 144, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 145, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 146, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 147, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 148, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 162, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 163, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 164, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 165, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 166, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 167, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 248, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 387, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 389, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 401, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1060, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1063, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "line": 483, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 317, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 511, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 531, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 534, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 666, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 785, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 803, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 807, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 109, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 128, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 134, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 138, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 139, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 142, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 143, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 144, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 145, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 146, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 147, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 148, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 162, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 163, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 164, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 165, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 166, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 167, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 248, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 387, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 389, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 401, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1060, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1063, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "line": 483, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 317, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 511, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 531, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 534, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 666, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 785, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 803, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 807, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 109, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 128, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 134, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 138, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 139, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 142, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 143, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 144, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 145, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 146, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 147, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 148, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 162, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 163, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 164, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 165, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 166, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 167, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 109, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 128, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 134, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 138, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 139, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 142, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 143, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 144, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 145, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 146, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 147, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 148, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 162, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 163, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 164, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 165, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 166, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 167, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 248, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 387, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 389, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 401, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 248, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 387, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 389, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 401, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1060, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1063, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1060, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 1063, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "line": 483, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/manifest.js", + "line": 483, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 317, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 511, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 531, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 534, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 666, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 785, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 803, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 807, + "kind": "custom-event-listener", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 317, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 511, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 531, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 534, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 666, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 785, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 803, + "kind": "ipc-send", + "match": "on(\"messages\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/message.js", + "line": 807, + "kind": "ipc-send", + "match": "on(\"messages\"" + } + ] + }, + { + "name": "minimize-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 123, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 122, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 49, + "kind": "ipc-send" + }, + { + "file": "preloads/loom.js", + "line": 44, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 31, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 153, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 41, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 48, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 239, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 64, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 731, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 41, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 239, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 731, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 123, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "preloads/desktop.js", + "line": 122, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "preloads/docx.js", + "line": 49, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "preloads/loom.js", + "line": 44, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 31, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "preloads/utility.js", + "line": 153, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 41, + "kind": "custom-event-listener", + "match": "on('minimize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 41, + "kind": "ipc-send", + "match": "on('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/preload.js", + "line": 48, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 239, + "kind": "custom-event-listener", + "match": "on('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 239, + "kind": "ipc-send", + "match": "on('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 64, + "kind": "ipc-send", + "match": "send('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 731, + "kind": "custom-event-listener", + "match": "on('minimize-window'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 731, + "kind": "ipc-send", + "match": "on('minimize-window'" + } + ] + }, + { + "name": "Modal", + "producers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 356, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 356, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 356, + "kind": "custom-event-listener", + "match": "on('Modal'" + }, + { + "file": "modules/ui-system/component-showcase.js", + "line": 356, + "kind": "ipc-send", + "match": "on('Modal'" + } + ] + }, + { + "name": "modal-ready", + "producers": [ + { + "file": "modules/ui-helpers.js", + "line": 340, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/event-listeners.js", + "line": 581, + "kind": "custom-event-listener" + }, + { + "file": "modules/filterManager.js", + "line": 645, + "kind": "custom-event-listener" + }, + { + "file": "modules/searchManager.js", + "line": 43, + "kind": "custom-event-listener" + }, + { + "file": "modules/settingsManager.js", + "line": 813, + "kind": "custom-event-listener" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 451, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 581, + "kind": "custom-event-listener", + "match": "addEventListener('modal-ready'" + }, + { + "file": "modules/filterManager.js", + "line": 645, + "kind": "custom-event-listener", + "match": "addEventListener('modal-ready'" + }, + { + "file": "modules/searchManager.js", + "line": 43, + "kind": "custom-event-listener", + "match": "addEventListener('modal-ready'" + }, + { + "file": "modules/settingsManager.js", + "line": 813, + "kind": "custom-event-listener", + "match": "addEventListener('modal-ready'" + }, + { + "file": "modules/ui-helpers.js", + "line": 340, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('modal-ready'" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 451, + "kind": "custom-event-listener", + "match": "addEventListener('modal-ready'" + } + ] + }, + { + "name": "modal-visibility-changed", + "producers": [ + { + "file": "modules/ui-helpers.js", + "line": 349, + "kind": "custom-event-dispatch" + }, + { + "file": "modules/ui-helpers.js", + "line": 367, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/ui-system/next-shell/overlay-coordinator.js", + "line": 48, + "kind": "custom-event-listener" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 446, + "kind": "custom-event-listener" + }, + { + "file": "modules/ui-system/vcp-main-ui-runtime.js", + "line": 55, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ui-helpers.js", + "line": 349, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('modal-visibility-changed'" + }, + { + "file": "modules/ui-helpers.js", + "line": 367, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('modal-visibility-changed'" + }, + { + "file": "modules/ui-system/next-shell/overlay-coordinator.js", + "line": 48, + "kind": "custom-event-listener", + "match": "addEventListener('modal-visibility-changed'" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 446, + "kind": "custom-event-listener", + "match": "addEventListener('modal-visibility-changed'" + }, + { + "file": "modules/ui-system/vcp-main-ui-runtime.js", + "line": 55, + "kind": "custom-event-listener", + "match": "addEventListener('modal-visibility-changed'" + } + ] + }, + { + "name": "music-cancel-preload", + "producers": [ + { + "file": "preloads/chat.js", + "line": 352, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 343, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 255, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 390, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 352, + "kind": "ipc-send", + "match": "invoke('music-cancel-preload'" + }, + { + "file": "preloads/desktop.js", + "line": 343, + "kind": "ipc-send", + "match": "invoke('music-cancel-preload'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 255, + "kind": "ipc-send", + "match": "invoke('music-cancel-preload'" + }, + { + "file": "preloads/utility.js", + "line": 390, + "kind": "ipc-send", + "match": "invoke('music-cancel-preload'" + } + ] + }, + { + "name": "music-get-settings", + "producers": [ + { + "file": "preloads/chat.js", + "line": 372, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 363, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 275, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 410, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 372, + "kind": "ipc-send", + "match": "invoke('music-get-settings'" + }, + { + "file": "preloads/desktop.js", + "line": 363, + "kind": "ipc-send", + "match": "invoke('music-get-settings'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 275, + "kind": "ipc-send", + "match": "invoke('music-get-settings'" + }, + { + "file": "preloads/utility.js", + "line": 410, + "kind": "ipc-send", + "match": "invoke('music-get-settings'" + } + ] + }, + { + "name": "music-save-settings", + "producers": [ + { + "file": "preloads/chat.js", + "line": 373, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 364, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 276, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 411, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 373, + "kind": "ipc-send", + "match": "invoke('music-save-settings'" + }, + { + "file": "preloads/desktop.js", + "line": 364, + "kind": "ipc-send", + "match": "invoke('music-save-settings'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 276, + "kind": "ipc-send", + "match": "invoke('music-save-settings'" + }, + { + "file": "preloads/utility.js", + "line": 411, + "kind": "ipc-send", + "match": "invoke('music-save-settings'" + } + ] + }, + { + "name": "notification-filter-changed", + "producers": [ + { + "file": "modules/filterManager.js", + "line": 27, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/event-listeners.js", + "line": 1257, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 1257, + "kind": "custom-event-listener", + "match": "addEventListener('notification-filter-changed'" + }, + { + "file": "modules/filterManager.js", + "line": 27, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('notification-filter-changed'" + } + ] + }, + { + "name": "onCreateUnlockedTopic(", + "producers": [], + "consumers": [ + { + "file": "modules/event-listeners.js", + "line": 1680, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 1680, + "kind": "preload-subscription", + "match": "onCreateUnlockedTopic(" + } + ] + }, + { + "name": "onDesktopStatus(", + "producers": [], + "consumers": [ + { + "file": "modules/renderer/desktopPushConsumer.js", + "line": 51, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/renderer/desktopPushConsumer.js", + "line": 51, + "kind": "preload-subscription", + "match": "onDesktopStatus(" + } + ] + }, + { + "name": "onDoToggleNotificationsSidebar(", + "producers": [], + "consumers": [ + { + "file": "modules/event-listeners.js", + "line": 1391, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 1391, + "kind": "preload-subscription", + "match": "onDoToggleNotificationsSidebar(" + } + ] + }, + { + "name": "onFilterSettings(", + "producers": [], + "consumers": [ + { + "file": "modules/mainChatCommands.js", + "line": 84, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/mainChatCommands.js", + "line": 84, + "kind": "preload-subscription", + "match": "onFilterSettings(" + } + ] + }, + { + "name": "onMessage(", + "producers": [], + "consumers": [ + { + "file": "preloads/chat.js", + "line": 56, + "kind": "preload-subscription" + }, + { + "file": "preloads/chat.js", + "line": 61, + "kind": "preload-subscription" + }, + { + "file": "preloads/desktop.js", + "line": 56, + "kind": "preload-subscription" + }, + { + "file": "preloads/desktop.js", + "line": 61, + "kind": "preload-subscription" + }, + { + "file": "preloads/shared/apiFactory.js", + "line": 56, + "kind": "preload-subscription" + }, + { + "file": "preloads/shared/apiFactory.js", + "line": 61, + "kind": "preload-subscription" + }, + { + "file": "preloads/utility.js", + "line": 85, + "kind": "preload-subscription" + }, + { + "file": "preloads/utility.js", + "line": 90, + "kind": "preload-subscription" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 928, + "kind": "preload-subscription" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 972, + "kind": "preload-subscription" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 194, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 56, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/chat.js", + "line": 61, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/desktop.js", + "line": 56, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/desktop.js", + "line": 61, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/shared/apiFactory.js", + "line": 56, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/shared/apiFactory.js", + "line": 61, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/utility.js", + "line": 85, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "preloads/utility.js", + "line": 90, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 928, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "modules/renderer/contentProcessor.js", + "line": 972, + "kind": "preload-subscription", + "match": "onMessage(" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/transport/websocket.js", + "line": 194, + "kind": "preload-subscription", + "match": "onMessage(" + } + ] + }, + { + "name": "onRetryableRestart(", + "producers": [], + "consumers": [ + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 196, + "kind": "preload-subscription" + }, + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 213, + "kind": "preload-subscription" + }, + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 245, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 196, + "kind": "preload-subscription", + "match": "onRetryableRestart(" + }, + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 213, + "kind": "preload-subscription", + "match": "onRetryableRestart(" + }, + { + "file": "modules/services/chatDataService/lifecycle.js", + "line": 245, + "kind": "preload-subscription", + "match": "onRetryableRestart(" + } + ] + }, + { + "name": "onSelection(", + "producers": [], + "consumers": [ + { + "file": "modules/assistant/assistant-rust-adapter.js", + "line": 221, + "kind": "preload-subscription" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 509, + "kind": "preload-subscription" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 612, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/assistant/assistant-rust-adapter.js", + "line": 221, + "kind": "preload-subscription", + "match": "onSelection(" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 509, + "kind": "preload-subscription", + "match": "onSelection(" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 612, + "kind": "preload-subscription", + "match": "onSelection(" + } + ] + }, + { + "name": "onStreamEnd(", + "producers": [], + "consumers": [ + { + "file": "modules/vcpClient.js", + "line": 421, + "kind": "preload-subscription" + }, + { + "file": "modules/vcpClient.js", + "line": 464, + "kind": "preload-subscription" + }, + { + "file": "modules/vcpClient.js", + "line": 481, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/vcpClient.js", + "line": 421, + "kind": "preload-subscription", + "match": "onStreamEnd(" + }, + { + "file": "modules/vcpClient.js", + "line": 464, + "kind": "preload-subscription", + "match": "onStreamEnd(" + }, + { + "file": "modules/vcpClient.js", + "line": 481, + "kind": "preload-subscription", + "match": "onStreamEnd(" + } + ] + }, + { + "name": "onStreamingEventConsumer(", + "producers": [], + "consumers": [ + { + "file": "renderer.js", + "line": 599, + "kind": "preload-subscription" + }, + { + "file": "modules/renderer/nonStreamingEventConsumer.js", + "line": 2, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "renderer.js", + "line": 599, + "kind": "preload-subscription", + "match": "onStreamingEventConsumer(" + }, + { + "file": "modules/renderer/nonStreamingEventConsumer.js", + "line": 2, + "kind": "preload-subscription", + "match": "onStreamingEventConsumer(" + } + ] + }, + { + "name": "onThemeInit(", + "producers": [], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.js", + "line": 355, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/ShellViewer.js", + "line": 355, + "kind": "preload-subscription", + "match": "onThemeInit(" + } + ] + }, + { + "name": "onThemeUpdated(", + "producers": [], + "consumers": [ + { + "file": "modules/image-viewer.js", + "line": 84, + "kind": "preload-subscription" + }, + { + "file": "modules/text-viewer.js", + "line": 1066, + "kind": "preload-subscription" + }, + { + "file": "modules/uiManager.js", + "line": 151, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/image-viewer.js", + "line": 84, + "kind": "preload-subscription", + "match": "onThemeUpdated(" + }, + { + "file": "modules/text-viewer.js", + "line": 1066, + "kind": "preload-subscription", + "match": "onThemeUpdated(" + }, + { + "file": "modules/uiManager.js", + "line": 151, + "kind": "preload-subscription", + "match": "onThemeUpdated(" + } + ] + }, + { + "name": "onVCPStreamEvent(", + "producers": [], + "consumers": [ + { + "file": "modules/renderer/mainChatEventBridge.js", + "line": 19, + "kind": "preload-subscription" + } + ], + "evidence": [ + { + "file": "modules/renderer/mainChatEventBridge.js", + "line": 19, + "kind": "preload-subscription", + "match": "onVCPStreamEvent(" + } + ] + }, + { + "name": "open-canvas-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 163, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 159, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 70, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 202, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 163, + "kind": "ipc-send", + "match": "invoke('open-canvas-window'" + }, + { + "file": "preloads/desktop.js", + "line": 159, + "kind": "ipc-send", + "match": "invoke('open-canvas-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 70, + "kind": "ipc-send", + "match": "invoke('open-canvas-window'" + }, + { + "file": "preloads/utility.js", + "line": 202, + "kind": "ipc-send", + "match": "invoke('open-canvas-window'" + } + ] + }, + { + "name": "open-desktop-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 164, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 160, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 71, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 203, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 164, + "kind": "ipc-send", + "match": "invoke('open-desktop-window'" + }, + { + "file": "preloads/desktop.js", + "line": 160, + "kind": "ipc-send", + "match": "invoke('open-desktop-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 71, + "kind": "ipc-send", + "match": "invoke('open-desktop-window'" + }, + { + "file": "preloads/utility.js", + "line": 203, + "kind": "ipc-send", + "match": "invoke('open-desktop-window'" + } + ] + }, + { + "name": "open-dice-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 162, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 158, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 69, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 201, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 162, + "kind": "ipc-send", + "match": "invoke('open-dice-window'" + }, + { + "file": "preloads/desktop.js", + "line": 158, + "kind": "ipc-send", + "match": "invoke('open-dice-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 69, + "kind": "ipc-send", + "match": "invoke('open-dice-window'" + }, + { + "file": "preloads/utility.js", + "line": 201, + "kind": "ipc-send", + "match": "invoke('open-dice-window'" + } + ] + }, + { + "name": "open-docx-window", + "producers": [ + { + "file": "preloads/docx.js", + "line": 13, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/docx.js", + "line": 13, + "kind": "ipc-send", + "match": "invoke('open-docx-window'" + } + ] + }, + { + "name": "open-forum-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 159, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 155, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 65, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 197, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 261, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 261, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 159, + "kind": "ipc-send", + "match": "send('open-forum-window'" + }, + { + "file": "preloads/desktop.js", + "line": 155, + "kind": "ipc-send", + "match": "send('open-forum-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 65, + "kind": "ipc-send", + "match": "send('open-forum-window'" + }, + { + "file": "preloads/utility.js", + "line": 197, + "kind": "ipc-send", + "match": "send('open-forum-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 261, + "kind": "custom-event-listener", + "match": "on('open-forum-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 261, + "kind": "ipc-send", + "match": "on('open-forum-window'" + } + ] + }, + { + "name": "open-image-in-new-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 139, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 138, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 47, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 179, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/fileDialogHandlers.js", + "line": 354, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/fileDialogHandlers.js", + "line": 354, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 139, + "kind": "ipc-send", + "match": "send('open-image-in-new-window'" + }, + { + "file": "preloads/desktop.js", + "line": 138, + "kind": "ipc-send", + "match": "send('open-image-in-new-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 47, + "kind": "ipc-send", + "match": "send('open-image-in-new-window'" + }, + { + "file": "preloads/utility.js", + "line": 179, + "kind": "ipc-send", + "match": "send('open-image-in-new-window'" + }, + { + "file": "modules/ipc/fileDialogHandlers.js", + "line": 354, + "kind": "custom-event-listener", + "match": "on('open-image-in-new-window'" + }, + { + "file": "modules/ipc/fileDialogHandlers.js", + "line": 354, + "kind": "ipc-send", + "match": "on('open-image-in-new-window'" + } + ] + }, + { + "name": "open-log-window", + "producers": [ + { + "file": "preloads/shared/catalog.js", + "line": 67, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 199, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 375, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 375, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/shared/catalog.js", + "line": 67, + "kind": "ipc-send", + "match": "send('open-log-window'" + }, + { + "file": "preloads/utility.js", + "line": 199, + "kind": "ipc-send", + "match": "send('open-log-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 375, + "kind": "custom-event-listener", + "match": "on('open-log-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 375, + "kind": "ipc-send", + "match": "on('open-log-window'" + } + ] + }, + { + "name": "open-memo-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 160, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 156, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 66, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 198, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 318, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 318, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 160, + "kind": "ipc-send", + "match": "send('open-memo-window'" + }, + { + "file": "preloads/desktop.js", + "line": 156, + "kind": "ipc-send", + "match": "send('open-memo-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 66, + "kind": "ipc-send", + "match": "send('open-memo-window'" + }, + { + "file": "preloads/utility.js", + "line": 198, + "kind": "ipc-send", + "match": "send('open-memo-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 318, + "kind": "custom-event-listener", + "match": "on('open-memo-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 318, + "kind": "ipc-send", + "match": "on('open-memo-window'" + } + ] + }, + { + "name": "open-music-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 161, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 157, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 68, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 200, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/musicHandlers.js", + "line": 290, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/musicHandlers.js", + "line": 290, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 161, + "kind": "ipc-send", + "match": "send('open-music-window'" + }, + { + "file": "preloads/desktop.js", + "line": 157, + "kind": "ipc-send", + "match": "send('open-music-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 68, + "kind": "ipc-send", + "match": "send('open-music-window'" + }, + { + "file": "preloads/utility.js", + "line": 200, + "kind": "ipc-send", + "match": "send('open-music-window'" + }, + { + "file": "modules/ipc/musicHandlers.js", + "line": 290, + "kind": "custom-event-listener", + "match": "on('open-music-window'" + }, + { + "file": "modules/ipc/musicHandlers.js", + "line": 290, + "kind": "ipc-send", + "match": "on('open-music-window'" + } + ] + }, + { + "name": "open-note-mini-window", + "producers": [ + { + "file": "preloads/utility.js", + "line": 191, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 191, + "kind": "ipc-send", + "match": "invoke('open-note-mini-window'" + } + ] + }, + { + "name": "open-notes-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 153, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 149, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 59, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 190, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 153, + "kind": "ipc-send", + "match": "invoke('open-notes-window'" + }, + { + "file": "preloads/desktop.js", + "line": 149, + "kind": "ipc-send", + "match": "invoke('open-notes-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 59, + "kind": "ipc-send", + "match": "invoke('open-notes-window'" + }, + { + "file": "preloads/utility.js", + "line": 190, + "kind": "ipc-send", + "match": "invoke('open-notes-window'" + } + ] + }, + { + "name": "open-python-attachment-in-text-editor", + "producers": [ + { + "file": "preloads/chat.js", + "line": 142, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 50, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 142, + "kind": "ipc-send", + "match": "invoke('open-python-attachment-in-text-editor'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 50, + "kind": "ipc-send", + "match": "invoke('open-python-attachment-in-text-editor'" + } + ] + }, + { + "name": "open-rag-observer-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 156, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 152, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 62, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 194, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 156, + "kind": "ipc-send", + "match": "invoke('open-rag-observer-window'" + }, + { + "file": "preloads/desktop.js", + "line": 152, + "kind": "ipc-send", + "match": "invoke('open-rag-observer-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 62, + "kind": "ipc-send", + "match": "invoke('open-rag-observer-window'" + }, + { + "file": "preloads/utility.js", + "line": 194, + "kind": "ipc-send", + "match": "invoke('open-rag-observer-window'" + } + ] + }, + { + "name": "open-task-window", + "producers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 430, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 430, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 430, + "kind": "custom-event-listener", + "match": "on('open-task-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 430, + "kind": "ipc-send", + "match": "on('open-task-window'" + } + ] + }, + { + "name": "open-themes-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 157, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 153, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 63, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 195, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 208, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/themeHandlers.js", + "line": 208, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 157, + "kind": "ipc-send", + "match": "send('open-themes-window'" + }, + { + "file": "preloads/desktop.js", + "line": 153, + "kind": "ipc-send", + "match": "send('open-themes-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 63, + "kind": "ipc-send", + "match": "send('open-themes-window'" + }, + { + "file": "preloads/utility.js", + "line": 195, + "kind": "ipc-send", + "match": "send('open-themes-window'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 208, + "kind": "custom-event-listener", + "match": "on('open-themes-window'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 208, + "kind": "ipc-send", + "match": "on('open-themes-window'" + } + ] + }, + { + "name": "open-translator-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 155, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 151, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 61, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 193, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 155, + "kind": "ipc-send", + "match": "invoke('open-translator-window'" + }, + { + "file": "preloads/desktop.js", + "line": 151, + "kind": "ipc-send", + "match": "invoke('open-translator-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 61, + "kind": "ipc-send", + "match": "invoke('open-translator-window'" + }, + { + "file": "preloads/utility.js", + "line": 193, + "kind": "ipc-send", + "match": "invoke('open-translator-window'" + } + ] + }, + { + "name": "open-voice-chat-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 158, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 154, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 64, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 196, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/voiceHandlers.js", + "line": 107, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/voiceHandlers.js", + "line": 107, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 158, + "kind": "ipc-send", + "match": "send('open-voice-chat-window'" + }, + { + "file": "preloads/desktop.js", + "line": 154, + "kind": "ipc-send", + "match": "send('open-voice-chat-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 64, + "kind": "ipc-send", + "match": "send('open-voice-chat-window'" + }, + { + "file": "preloads/utility.js", + "line": 196, + "kind": "ipc-send", + "match": "send('open-voice-chat-window'" + }, + { + "file": "modules/ipc/voiceHandlers.js", + "line": 107, + "kind": "custom-event-listener", + "match": "on('open-voice-chat-window'" + }, + { + "file": "modules/ipc/voiceHandlers.js", + "line": 107, + "kind": "ipc-send", + "match": "on('open-voice-chat-window'" + } + ] + }, + { + "name": "plugin-manager-list-plugins", + "producers": [ + { + "file": "preloads/utility.js", + "line": 469, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 469, + "kind": "ipc-send", + "match": "invoke('plugin-manager-list-plugins'" + } + ] + }, + { + "name": "plugin-manager-open-plugin-folder", + "producers": [ + { + "file": "preloads/utility.js", + "line": 473, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 473, + "kind": "ipc-send", + "match": "invoke('plugin-manager-open-plugin-folder'" + } + ] + }, + { + "name": "plugin-manager-save-config-env", + "producers": [ + { + "file": "preloads/utility.js", + "line": 471, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 471, + "kind": "ipc-send", + "match": "invoke('plugin-manager-save-config-env'" + } + ] + }, + { + "name": "plugin-manager-save-manifest", + "producers": [ + { + "file": "preloads/utility.js", + "line": 470, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 470, + "kind": "ipc-send", + "match": "invoke('plugin-manager-save-manifest'" + } + ] + }, + { + "name": "plugin-manager-set-plugin-enabled", + "producers": [ + { + "file": "preloads/utility.js", + "line": 472, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/utility.js", + "line": 472, + "kind": "ipc-send", + "match": "invoke('plugin-manager-set-plugin-enabled'" + } + ] + }, + { + "name": "rag-overlay-set-notification-category-enabled", + "producers": [ + { + "file": "preloads/shared/catalog.js", + "line": 222, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 357, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/ragHandlers.js", + "line": 434, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/ragHandlers.js", + "line": 434, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/shared/catalog.js", + "line": 222, + "kind": "ipc-send", + "match": "send('rag-overlay-set-notification-category-enabled'" + }, + { + "file": "preloads/utility.js", + "line": 357, + "kind": "ipc-send", + "match": "send('rag-overlay-set-notification-category-enabled'" + }, + { + "file": "modules/ipc/ragHandlers.js", + "line": 434, + "kind": "custom-event-listener", + "match": "on('rag-overlay-set-notification-category-enabled'" + }, + { + "file": "modules/ipc/ragHandlers.js", + "line": 434, + "kind": "ipc-send", + "match": "on('rag-overlay-set-notification-category-enabled'" + } + ] + }, + { + "name": "redo-group-chat-message", + "producers": [ + { + "file": "preloads/chat.js", + "line": 235, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 230, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 141, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 274, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 235, + "kind": "ipc-send", + "match": "invoke('redo-group-chat-message'" + }, + { + "file": "preloads/desktop.js", + "line": 230, + "kind": "ipc-send", + "match": "invoke('redo-group-chat-message'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 141, + "kind": "ipc-send", + "match": "invoke('redo-group-chat-message'" + }, + { + "file": "preloads/utility.js", + "line": 274, + "kind": "ipc-send", + "match": "invoke('redo-group-chat-message'" + } + ] + }, + { + "name": "reload-agent-settings", + "producers": [ + { + "file": "modules/ipc/promptHandlers.js", + "line": 299, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/promptHandlers.js", + "line": 299, + "kind": "ipc-send", + "match": "send('reload-agent-settings'" + } + ] + }, + { + "name": "restore-flowlock-claim", + "producers": [ + { + "file": "preloads/chat.js", + "line": 268, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 268, + "kind": "ipc-send", + "match": "invoke('restore-flowlock-claim'" + } + ] + }, + { + "name": "save-agent-topic-title", + "producers": [ + { + "file": "preloads/chat.js", + "line": 193, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 189, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 100, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 233, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 193, + "kind": "ipc-send", + "match": "invoke('save-agent-topic-title'" + }, + { + "file": "preloads/desktop.js", + "line": 189, + "kind": "ipc-send", + "match": "invoke('save-agent-topic-title'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 100, + "kind": "ipc-send", + "match": "invoke('save-agent-topic-title'" + }, + { + "file": "preloads/utility.js", + "line": 233, + "kind": "ipc-send", + "match": "invoke('save-agent-topic-title'" + } + ] + }, + { + "name": "save-chat-history", + "producers": [ + { + "file": "preloads/chat.js", + "line": 200, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 196, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 107, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 240, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 200, + "kind": "ipc-send", + "match": "invoke('save-chat-history'" + }, + { + "file": "preloads/desktop.js", + "line": 196, + "kind": "ipc-send", + "match": "invoke('save-chat-history'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 107, + "kind": "ipc-send", + "match": "invoke('save-chat-history'" + }, + { + "file": "preloads/utility.js", + "line": 240, + "kind": "ipc-send", + "match": "invoke('save-chat-history'" + } + ] + }, + { + "name": "save-group-chat-history", + "producers": [ + { + "file": "preloads/chat.js", + "line": 228, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 223, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 134, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 267, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 228, + "kind": "ipc-send", + "match": "invoke('save-group-chat-history'" + }, + { + "file": "preloads/desktop.js", + "line": 223, + "kind": "ipc-send", + "match": "invoke('save-group-chat-history'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 134, + "kind": "ipc-send", + "match": "invoke('save-group-chat-history'" + }, + { + "file": "preloads/utility.js", + "line": 267, + "kind": "ipc-send", + "match": "invoke('save-group-chat-history'" + } + ] + }, + { + "name": "save-group-topic-order", + "producers": [ + { + "file": "preloads/chat.js", + "line": 232, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 227, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 138, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 271, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 232, + "kind": "ipc-send", + "match": "invoke('save-group-topic-order'" + }, + { + "file": "preloads/desktop.js", + "line": 227, + "kind": "ipc-send", + "match": "invoke('save-group-topic-order'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 138, + "kind": "ipc-send", + "match": "invoke('save-group-topic-order'" + }, + { + "file": "preloads/utility.js", + "line": 271, + "kind": "ipc-send", + "match": "invoke('save-group-topic-order'" + } + ] + }, + { + "name": "save-group-topic-title", + "producers": [ + { + "file": "preloads/chat.js", + "line": 226, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 221, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 132, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 265, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 226, + "kind": "ipc-send", + "match": "invoke('save-group-topic-title'" + }, + { + "file": "preloads/desktop.js", + "line": 221, + "kind": "ipc-send", + "match": "invoke('save-group-topic-title'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 132, + "kind": "ipc-send", + "match": "invoke('save-group-topic-title'" + }, + { + "file": "preloads/utility.js", + "line": 265, + "kind": "ipc-send", + "match": "invoke('save-group-topic-title'" + } + ] + }, + { + "name": "save-rust-assistant-config", + "producers": [ + { + "file": "preloads/chat.js", + "line": 248, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 243, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 154, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 287, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 248, + "kind": "ipc-send", + "match": "invoke('save-rust-assistant-config'" + }, + { + "file": "preloads/desktop.js", + "line": 243, + "kind": "ipc-send", + "match": "invoke('save-rust-assistant-config'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 154, + "kind": "ipc-send", + "match": "invoke('save-rust-assistant-config'" + }, + { + "file": "preloads/utility.js", + "line": 287, + "kind": "ipc-send", + "match": "invoke('save-rust-assistant-config'" + } + ] + }, + { + "name": "save-settings", + "producers": [ + { + "file": "preloads/chat.js", + "line": 106, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 105, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 14, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 136, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 106, + "kind": "ipc-send", + "match": "invoke('save-settings'" + }, + { + "file": "preloads/desktop.js", + "line": 105, + "kind": "ipc-send", + "match": "invoke('save-settings'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 14, + "kind": "ipc-send", + "match": "invoke('save-settings'" + }, + { + "file": "preloads/utility.js", + "line": 136, + "kind": "ipc-send", + "match": "invoke('save-settings'" + } + ] + }, + { + "name": "save-topic-order", + "producers": [ + { + "file": "preloads/chat.js", + "line": 211, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 206, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 117, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 250, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 211, + "kind": "ipc-send", + "match": "invoke('save-topic-order'" + }, + { + "file": "preloads/desktop.js", + "line": 206, + "kind": "ipc-send", + "match": "invoke('save-topic-order'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 117, + "kind": "ipc-send", + "match": "invoke('save-topic-order'" + }, + { + "file": "preloads/utility.js", + "line": 250, + "kind": "ipc-send", + "match": "invoke('save-topic-order'" + } + ] + }, + { + "name": "save-translator-settings", + "producers": [ + { + "file": "preloads/shared/catalog.js", + "line": 13, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 135, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/shared/catalog.js", + "line": 13, + "kind": "ipc-send", + "match": "invoke('save-translator-settings'" + }, + { + "file": "preloads/utility.js", + "line": 135, + "kind": "ipc-send", + "match": "invoke('save-translator-settings'" + } + ] + }, + { + "name": "search-topics-by-content", + "producers": [ + { + "file": "preloads/chat.js", + "line": 233, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 228, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 139, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 272, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 233, + "kind": "ipc-send", + "match": "invoke('search-topics-by-content'" + }, + { + "file": "preloads/desktop.js", + "line": 228, + "kind": "ipc-send", + "match": "invoke('search-topics-by-content'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 139, + "kind": "ipc-send", + "match": "invoke('search-topics-by-content'" + }, + { + "file": "preloads/utility.js", + "line": 272, + "kind": "ipc-send", + "match": "invoke('search-topics-by-content'" + } + ] + }, + { + "name": "send-group-chat-message", + "producers": [ + { + "file": "preloads/chat.js", + "line": 229, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 224, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 135, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 268, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 229, + "kind": "ipc-send", + "match": "invoke('send-group-chat-message'" + }, + { + "file": "preloads/desktop.js", + "line": 224, + "kind": "ipc-send", + "match": "invoke('send-group-chat-message'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 135, + "kind": "ipc-send", + "match": "invoke('send-group-chat-message'" + }, + { + "file": "preloads/utility.js", + "line": 268, + "kind": "ipc-send", + "match": "invoke('send-group-chat-message'" + } + ] + }, + { + "name": "send-to-vcp", + "producers": [ + { + "file": "preloads/chat.js", + "line": 213, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 208, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 119, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 252, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 213, + "kind": "ipc-send", + "match": "invoke('send-to-vcp'" + }, + { + "file": "preloads/desktop.js", + "line": 208, + "kind": "ipc-send", + "match": "invoke('send-to-vcp'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 119, + "kind": "ipc-send", + "match": "invoke('send-to-vcp'" + }, + { + "file": "preloads/utility.js", + "line": 252, + "kind": "ipc-send", + "match": "invoke('send-to-vcp'" + } + ] + }, + { + "name": "send-vcplog-message", + "producers": [ + { + "file": "main.js", + "line": 1703, + "kind": "ipc-send" + }, + { + "file": "preloads/chat.js", + "line": 242, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 237, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 148, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 281, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "main.js", + "line": 1703, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "main.js", + "line": 1703, + "kind": "custom-event-listener", + "match": "on('send-vcplog-message'" + }, + { + "file": "main.js", + "line": 1703, + "kind": "ipc-send", + "match": "on('send-vcplog-message'" + }, + { + "file": "preloads/chat.js", + "line": 242, + "kind": "ipc-send", + "match": "send('send-vcplog-message'" + }, + { + "file": "preloads/desktop.js", + "line": 237, + "kind": "ipc-send", + "match": "send('send-vcplog-message'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 148, + "kind": "ipc-send", + "match": "send('send-vcplog-message'" + }, + { + "file": "preloads/utility.js", + "line": 281, + "kind": "ipc-send", + "match": "send('send-vcplog-message'" + } + ] + }, + { + "name": "sendErrorToElectron", + "producers": [ + { + "file": "modules/speechRecognizer.js", + "line": 134, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/speechRecognizer.js", + "line": 134, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/speechRecognizer.js", + "line": 134, + "kind": "custom-event-listener", + "match": "on('sendErrorToElectron'" + }, + { + "file": "modules/speechRecognizer.js", + "line": 134, + "kind": "ipc-send", + "match": "on('sendErrorToElectron'" + } + ] + }, + { + "name": "sendTextToElectron", + "producers": [ + { + "file": "modules/speechRecognizer.js", + "line": 128, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/speechRecognizer.js", + "line": 128, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/speechRecognizer.js", + "line": 128, + "kind": "custom-event-listener", + "match": "on('sendTextToElectron'" + }, + { + "file": "modules/speechRecognizer.js", + "line": 128, + "kind": "ipc-send", + "match": "on('sendTextToElectron'" + } + ] + }, + { + "name": "set-theme", + "producers": [ + { + "file": "preloads/chat.js", + "line": 145, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 143, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 53, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 184, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/settingsHandlers.js", + "line": 274, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/settingsHandlers.js", + "line": 274, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 145, + "kind": "ipc-send", + "match": "send('set-theme'" + }, + { + "file": "preloads/desktop.js", + "line": 143, + "kind": "ipc-send", + "match": "send('set-theme'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 53, + "kind": "ipc-send", + "match": "send('set-theme'" + }, + { + "file": "preloads/utility.js", + "line": 184, + "kind": "ipc-send", + "match": "send('set-theme'" + }, + { + "file": "modules/ipc/settingsHandlers.js", + "line": 274, + "kind": "custom-event-listener", + "match": "on('set-theme'" + }, + { + "file": "modules/ipc/settingsHandlers.js", + "line": 274, + "kind": "ipc-send", + "match": "on('set-theme'" + } + ] + }, + { + "name": "set-theme-mode", + "producers": [ + { + "file": "preloads/chat.js", + "line": 146, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 144, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 54, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 185, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 209, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/themeHandlers.js", + "line": 209, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 146, + "kind": "ipc-send", + "match": "send('set-theme-mode'" + }, + { + "file": "preloads/desktop.js", + "line": 144, + "kind": "ipc-send", + "match": "send('set-theme-mode'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 54, + "kind": "ipc-send", + "match": "send('set-theme-mode'" + }, + { + "file": "preloads/utility.js", + "line": 185, + "kind": "ipc-send", + "match": "send('set-theme-mode'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 209, + "kind": "custom-event-listener", + "match": "on('set-theme-mode'" + }, + { + "file": "modules/ipc/themeHandlers.js", + "line": 209, + "kind": "ipc-send", + "match": "on('set-theme-mode'" + } + ] + }, + { + "name": "set-topic-unread", + "producers": [ + { + "file": "preloads/chat.js", + "line": 197, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 193, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 104, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 237, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 197, + "kind": "ipc-send", + "match": "invoke('set-topic-unread'" + }, + { + "file": "preloads/desktop.js", + "line": 193, + "kind": "ipc-send", + "match": "invoke('set-topic-unread'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 104, + "kind": "ipc-send", + "match": "invoke('set-topic-unread'" + }, + { + "file": "preloads/utility.js", + "line": 237, + "kind": "ipc-send", + "match": "invoke('set-topic-unread'" + } + ] + }, + { + "name": "SettingsActionBar", + "producers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 285, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 285, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 285, + "kind": "custom-event-listener", + "match": "on('SettingsActionBar'" + }, + { + "file": "modules/ui-system/component-showcase.js", + "line": 285, + "kind": "ipc-send", + "match": "on('SettingsActionBar'" + } + ] + }, + { + "name": "SettingsSection", + "producers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 274, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 274, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 274, + "kind": "custom-event-listener", + "match": "on('SettingsSection'" + }, + { + "file": "modules/ui-system/component-showcase.js", + "line": 274, + "kind": "ipc-send", + "match": "on('SettingsSection'" + } + ] + }, + { + "name": "theme-init", + "producers": [ + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "line": 211, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 128, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 86, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 640, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "line": 211, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 86, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "line": 211, + "kind": "custom-event-listener", + "match": "on('theme-init'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/gui/PowerShellViewer.js", + "line": 211, + "kind": "ipc-send", + "match": "on('theme-init'" + }, + { + "file": "VCPDistributedServer/Plugin/PowerShellExecutor/PowerShellExecutor.js", + "line": 128, + "kind": "ipc-send", + "match": "send('theme-init'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 86, + "kind": "custom-event-listener", + "match": "on('theme-init'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/gui/preload.js", + "line": 86, + "kind": "ipc-send", + "match": "on('theme-init'" + }, + { + "file": "VCPDistributedServer/Plugin/PTYShellExecutor/PTYShellExecutor.impl.js", + "line": 640, + "kind": "ipc-send", + "match": "send('theme-init'" + } + ] + }, + { + "name": "theme-updated", + "producers": [ + { + "file": "modules/ipc/themeHandlers.js", + "line": 244, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/themeHandlers.js", + "line": 244, + "kind": "ipc-send", + "match": "send('theme-updated'" + } + ] + }, + { + "name": "toggle-notifications-sidebar", + "producers": [ + { + "file": "preloads/chat.js", + "line": 129, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 128, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 37, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 165, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 107, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 107, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 129, + "kind": "ipc-send", + "match": "send('toggle-notifications-sidebar'" + }, + { + "file": "preloads/desktop.js", + "line": 128, + "kind": "ipc-send", + "match": "send('toggle-notifications-sidebar'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 37, + "kind": "ipc-send", + "match": "send('toggle-notifications-sidebar'" + }, + { + "file": "preloads/utility.js", + "line": 165, + "kind": "ipc-send", + "match": "send('toggle-notifications-sidebar'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 107, + "kind": "custom-event-listener", + "match": "on('toggle-notifications-sidebar'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 107, + "kind": "ipc-send", + "match": "on('toggle-notifications-sidebar'" + } + ] + }, + { + "name": "toggle-selection-listener", + "producers": [ + { + "file": "preloads/chat.js", + "line": 243, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 238, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 149, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 282, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1176, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1176, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 243, + "kind": "ipc-send", + "match": "send('toggle-selection-listener'" + }, + { + "file": "preloads/desktop.js", + "line": 238, + "kind": "ipc-send", + "match": "send('toggle-selection-listener'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 149, + "kind": "ipc-send", + "match": "send('toggle-selection-listener'" + }, + { + "file": "preloads/utility.js", + "line": 282, + "kind": "ipc-send", + "match": "send('toggle-selection-listener'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1176, + "kind": "custom-event-listener", + "match": "on('toggle-selection-listener'" + }, + { + "file": "modules/ipc/assistantHandlers.js", + "line": 1176, + "kind": "ipc-send", + "match": "on('toggle-selection-listener'" + } + ] + }, + { + "name": "toggle-topic-lock", + "producers": [ + { + "file": "preloads/chat.js", + "line": 196, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 192, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 103, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 236, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 196, + "kind": "ipc-send", + "match": "invoke('toggle-topic-lock'" + }, + { + "file": "preloads/desktop.js", + "line": 192, + "kind": "ipc-send", + "match": "invoke('toggle-topic-lock'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 103, + "kind": "ipc-send", + "match": "invoke('toggle-topic-lock'" + }, + { + "file": "preloads/utility.js", + "line": 236, + "kind": "ipc-send", + "match": "invoke('toggle-topic-lock'" + } + ] + }, + { + "name": "topic_metadata", + "producers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 115, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 135, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 140, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 161, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 125, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 157, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 177, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 231, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 410, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 667, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 115, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 135, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 140, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 161, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 125, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 157, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 177, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 231, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 410, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 667, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 115, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 135, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 140, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 161, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 115, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 135, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 140, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 161, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 125, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 157, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 177, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 231, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 125, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 157, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 177, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/diff.js", + "line": 231, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 410, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 667, + "kind": "custom-event-listener", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 410, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/sync/entity.js", + "line": 667, + "kind": "ipc-send", + "match": "on(\"topic_metadata\"" + } + ] + }, + { + "name": "topic_validation", + "producers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 127, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 137, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 127, + "kind": "custom-event-listener" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 137, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 127, + "kind": "custom-event-listener", + "match": "on(\"topic_validation\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 137, + "kind": "custom-event-listener", + "match": "on(\"topic_validation\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 127, + "kind": "ipc-send", + "match": "on(\"topic_validation\"" + }, + { + "file": "VCPDistributedServer/Plugin/VCPMobileSync/error-contract.js", + "line": 137, + "kind": "ipc-send", + "match": "on(\"topic_validation\"" + } + ] + }, + { + "name": "unmaximize-window", + "producers": [ + { + "file": "preloads/chat.js", + "line": 125, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 124, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 51, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 33, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 155, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 59, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ipc/windowHandlers.js", + "line": 59, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 125, + "kind": "ipc-send", + "match": "send('unmaximize-window'" + }, + { + "file": "preloads/desktop.js", + "line": 124, + "kind": "ipc-send", + "match": "send('unmaximize-window'" + }, + { + "file": "preloads/docx.js", + "line": 51, + "kind": "ipc-send", + "match": "send('unmaximize-window'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 33, + "kind": "ipc-send", + "match": "send('unmaximize-window'" + }, + { + "file": "preloads/utility.js", + "line": 155, + "kind": "ipc-send", + "match": "send('unmaximize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 59, + "kind": "custom-event-listener", + "match": "on('unmaximize-window'" + }, + { + "file": "modules/ipc/windowHandlers.js", + "line": 59, + "kind": "ipc-send", + "match": "on('unmaximize-window'" + } + ] + }, + { + "name": "vchat-wallpaper-select-directory", + "producers": [ + { + "file": "preloads/chat.js", + "line": 424, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 424, + "kind": "ipc-send", + "match": "invoke('vchat-wallpaper-select-directory'" + } + ] + }, + { + "name": "vcp-appearance-changed", + "producers": [ + { + "file": "modules/ui-system/appearance-engine.js", + "line": 216, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/appearance-engine.js", + "line": 216, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-appearance-changed'" + } + ] + }, + { + "name": "vcp-appearance-studio-configure", + "producers": [ + { + "file": "renderer.js", + "line": 259, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/ui-system/appearance-studio.js", + "line": 7, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "renderer.js", + "line": 259, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-appearance-studio-configure'" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 7, + "kind": "custom-event-listener", + "match": "addEventListener('vcp-appearance-studio-configure'" + } + ] + }, + { + "name": "vcp-appearance-studio-dispose", + "producers": [ + { + "file": "renderer.js", + "line": 249, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/ui-system/appearance-studio.js", + "line": 10, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "renderer.js", + "line": 249, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-appearance-studio-dispose'" + }, + { + "file": "modules/ui-system/appearance-studio.js", + "line": 10, + "kind": "custom-event-listener", + "match": "addEventListener('vcp-appearance-studio-dispose'" + } + ] + }, + { + "name": "vcp-appearance-studio-preview", + "producers": [ + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1046, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/appearance-studio.js", + "line": 1046, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-appearance-studio-preview'" + } + ] + }, + { + "name": "vcp-chat-stream-terminal", + "producers": [ + { + "file": "renderer.js", + "line": 568, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "renderer.js", + "line": 568, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-chat-stream-terminal'" + } + ] + }, + { + "name": "vcp-frontend-plugin-ready", + "producers": [ + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 13, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 13, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-frontend-plugin-ready'" + } + ] + }, + { + "name": "vcp-frontend-plugins-loaded", + "producers": [ + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 61, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "VCPDistributedServer/frontend-plugin-loader.js", + "line": 61, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-frontend-plugins-loaded'" + } + ] + }, + { + "name": "vcp-log-message", + "producers": [ + { + "file": "main.js", + "line": 1637, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1651, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1654, + "kind": "ipc-send" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 331, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 1637, + "kind": "ipc-send", + "match": "send('vcp-log-message'" + }, + { + "file": "main.js", + "line": 1651, + "kind": "ipc-send", + "match": "send('vcp-log-message'" + }, + { + "file": "main.js", + "line": 1654, + "kind": "ipc-send", + "match": "send('vcp-log-message'" + }, + { + "file": "VCPDistributedServer/VCPDistributedServer.js", + "line": 331, + "kind": "ipc-send", + "match": "send('vcp-log-message'" + } + ] + }, + { + "name": "vcp-log-status", + "producers": [ + { + "file": "main.js", + "line": 1614, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1627, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1635, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1660, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1673, + "kind": "ipc-send" + }, + { + "file": "main.js", + "line": 1699, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 1614, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + }, + { + "file": "main.js", + "line": 1627, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + }, + { + "file": "main.js", + "line": 1635, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + }, + { + "file": "main.js", + "line": 1660, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + }, + { + "file": "main.js", + "line": 1673, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + }, + { + "file": "main.js", + "line": 1699, + "kind": "ipc-send", + "match": "send('vcp-log-status'" + } + ] + }, + { + "name": "vcp-main-chat-commands-configure", + "producers": [ + { + "file": "renderer.js", + "line": 785, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/mainChatCommands.js", + "line": 175, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "renderer.js", + "line": 785, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-main-chat-commands-configure'" + }, + { + "file": "modules/mainChatCommands.js", + "line": 175, + "kind": "custom-event-listener", + "match": "addEventListener('vcp-main-chat-commands-configure'" + } + ] + }, + { + "name": "vcp-renderer-ready", + "producers": [ + { + "file": "renderer.js", + "line": 963, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "renderer.js", + "line": 963, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-renderer-ready'" + } + ] + }, + { + "name": "vcp-settings-delete-result", + "producers": [ + { + "file": "modules/settingsManager.js", + "line": 103, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/settingsManager.js", + "line": 103, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-delete-result'" + } + ] + }, + { + "name": "vcp-settings-navigation-restored", + "producers": [ + { + "file": "modules/ui-system/settings-bridge.js", + "line": 431, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/event-listeners.js", + "line": 654, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 654, + "kind": "custom-event-listener", + "match": "addEventListener('vcp-settings-navigation-restored'" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 431, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-navigation-restored'" + } + ] + }, + { + "name": "vcp-settings-save-result", + "producers": [ + { + "file": "modules/event-listeners.js", + "line": 522, + "kind": "custom-event-dispatch" + }, + { + "file": "modules/global-settings-manager.js", + "line": 29, + "kind": "custom-event-dispatch" + }, + { + "file": "modules/settingsManager.js", + "line": 97, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/event-listeners.js", + "line": 522, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-save-result'" + }, + { + "file": "modules/global-settings-manager.js", + "line": 29, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-save-result'" + }, + { + "file": "modules/settingsManager.js", + "line": 97, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-save-result'" + } + ] + }, + { + "name": "vcp-settings-surface-updated", + "producers": [ + { + "file": "modules/settingsManager.js", + "line": 293, + "kind": "custom-event-dispatch" + } + ], + "consumers": [ + { + "file": "modules/ui-system/settings-bridge.js", + "line": 452, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/settingsManager.js", + "line": 293, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-settings-surface-updated'" + }, + { + "file": "modules/ui-system/settings-bridge.js", + "line": 452, + "kind": "custom-event-listener", + "match": "addEventListener('vcp-settings-surface-updated'" + } + ] + }, + { + "name": "vcp-stream-event", + "producers": [ + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 141, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 160, + "kind": "ipc-send" + }, + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 177, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 141, + "kind": "ipc-send", + "match": "send('vcp-stream-event'" + }, + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 160, + "kind": "ipc-send", + "match": "send('vcp-stream-event'" + }, + { + "file": "modules/ipc/groupChatHandlers.js", + "line": 177, + "kind": "ipc-send", + "match": "send('vcp-stream-event'" + } + ] + }, + { + "name": "vcp-ui-density-changed", + "producers": [ + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2363, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2363, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-ui-density-changed'" + } + ] + }, + { + "name": "vcp-ui-ready", + "producers": [ + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2384, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/vcp-ui.js", + "line": 2384, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-ui-ready'" + } + ] + }, + { + "name": "vcp-webawesome-adapter-ready", + "producers": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 247, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 247, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-webawesome-adapter-ready'" + } + ] + }, + { + "name": "vcp-webawesome-failed", + "producers": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 78, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 78, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-webawesome-failed'" + } + ] + }, + { + "name": "vcp-webawesome-loaded", + "producers": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 70, + "kind": "custom-event-dispatch" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 70, + "kind": "custom-event-dispatch", + "match": "dispatchEvent(new CustomEvent('vcp-webawesome-loaded'" + } + ] + }, + { + "name": "voice-chat-data", + "producers": [ + { + "file": "modules/ipc/voiceHandlers.js", + "line": 34, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "modules/ipc/voiceHandlers.js", + "line": 34, + "kind": "ipc-send", + "match": "send('voice-chat-data'" + } + ] + }, + { + "name": "window-all-closed", + "producers": [ + { + "file": "main.js", + "line": 1525, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "main.js", + "line": 1525, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "main.js", + "line": 1525, + "kind": "custom-event-listener", + "match": "on('window-all-closed'" + }, + { + "file": "main.js", + "line": 1525, + "kind": "ipc-send", + "match": "on('window-all-closed'" + } + ] + }, + { + "name": "window-lifecycle:ready", + "producers": [ + { + "file": "preloads/chat.js", + "line": 296, + "kind": "ipc-send" + }, + { + "file": "preloads/desktop.js", + "line": 288, + "kind": "ipc-send" + }, + { + "file": "preloads/docx.js", + "line": 44, + "kind": "ipc-send" + }, + { + "file": "preloads/shared/catalog.js", + "line": 199, + "kind": "ipc-send" + }, + { + "file": "preloads/utility.js", + "line": 334, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "preloads/chat.js", + "line": 296, + "kind": "ipc-send", + "match": "send('window-lifecycle:ready'" + }, + { + "file": "preloads/desktop.js", + "line": 288, + "kind": "ipc-send", + "match": "send('window-lifecycle:ready'" + }, + { + "file": "preloads/docx.js", + "line": 44, + "kind": "ipc-send", + "match": "send('window-lifecycle:ready'" + }, + { + "file": "preloads/shared/catalog.js", + "line": 199, + "kind": "ipc-send", + "match": "send('window-lifecycle:ready'" + }, + { + "file": "preloads/utility.js", + "line": 334, + "kind": "ipc-send", + "match": "send('window-lifecycle:ready'" + } + ] + }, + { + "name": "window-maximized", + "producers": [ + { + "file": "main.js", + "line": 620, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 620, + "kind": "ipc-send", + "match": "send('window-maximized'" + } + ] + }, + { + "name": "window-unmaximized", + "producers": [ + { + "file": "main.js", + "line": 625, + "kind": "ipc-send" + } + ], + "consumers": [], + "evidence": [ + { + "file": "main.js", + "line": 625, + "kind": "ipc-send", + "match": "send('window-unmaximized'" + } + ] + }, + { + "name": "WindowControls", + "producers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 429, + "kind": "ipc-send" + } + ], + "consumers": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 429, + "kind": "custom-event-listener" + } + ], + "evidence": [ + { + "file": "modules/ui-system/component-showcase.js", + "line": 429, + "kind": "custom-event-listener", + "match": "on('WindowControls'" + }, + { + "file": "modules/ui-system/component-showcase.js", + "line": 429, + "kind": "ipc-send", + "match": "on('WindowControls'" + } + ] + } + ], + "registeredDynamic": [ + { + "file": "modules/ui-system/webawesome-adapter.js", + "line": 150, + "reason": "dynamic CustomEvent name: vcpEventName", + "contractId": "ui.webawesome.dynamic-event-adapter" + } + ], + "undiscovered": [] +} diff --git a/docs/contracts/snapshots/chat-stream-cancel.json b/docs/contracts/snapshots/chat-stream-cancel.json new file mode 100644 index 000000000..9e122120c --- /dev/null +++ b/docs/contracts/snapshots/chat-stream-cancel.json @@ -0,0 +1,22 @@ +{ + "modelVisible": [ + { "role": "user", "content": "start a reply" }, + { "role": "assistant", "content": "partial", "terminal": "cancelled" } + ], + "chatVisible": { + "role": "assistant", + "status": "cancelled", + "content": "partial" + }, + "durable": { + "conversationKey": "snapshot-cancel-conversation", + "terminal": "cancelled", + "text": "partial" + }, + "projection": { + "statuses": ["streaming", "cancelled"], + "chunkCount": 1, + "text": "partial" + }, + "wire": ["chunk:partial", "cancelled"] +} diff --git a/docs/contracts/snapshots/chat-stream-discarded.json b/docs/contracts/snapshots/chat-stream-discarded.json new file mode 100644 index 000000000..96fc9c42f --- /dev/null +++ b/docs/contracts/snapshots/chat-stream-discarded.json @@ -0,0 +1,22 @@ +{ + "modelVisible": [ + { "role": "user", "content": "dispose the reply" }, + { "role": "assistant", "content": "partial", "terminal": "discarded" } + ], + "chatVisible": { + "role": "assistant", + "status": "discarded", + "content": "partial" + }, + "durable": { + "conversationKey": "snapshot-discarded-conversation", + "terminal": "discarded", + "text": "partial" + }, + "projection": { + "statuses": ["streaming", "discarded"], + "chunkCount": 1, + "text": "partial" + }, + "wire": ["chunk:partial", "discarded"] +} diff --git a/docs/contracts/snapshots/chat-stream-failed.json b/docs/contracts/snapshots/chat-stream-failed.json new file mode 100644 index 000000000..1406309f1 --- /dev/null +++ b/docs/contracts/snapshots/chat-stream-failed.json @@ -0,0 +1,22 @@ +{ + "modelVisible": [ + { "role": "user", "content": "trigger an error" }, + { "role": "assistant", "content": "partial", "terminal": "failed" } + ], + "chatVisible": { + "role": "assistant", + "status": "failed", + "content": "partial" + }, + "durable": { + "conversationKey": "snapshot-failed-conversation", + "terminal": "failed", + "text": "partial" + }, + "projection": { + "statuses": ["streaming", "failed"], + "chunkCount": 1, + "text": "partial" + }, + "wire": ["chunk:partial", "failed"] +} diff --git a/docs/contracts/snapshots/chat-stream.json b/docs/contracts/snapshots/chat-stream.json new file mode 100644 index 000000000..125a27986 --- /dev/null +++ b/docs/contracts/snapshots/chat-stream.json @@ -0,0 +1,22 @@ +{ + "modelVisible": [ + { "role": "user", "content": "say hello" }, + { "role": "assistant", "content": "hello world", "terminal": "completed" } + ], + "chatVisible": { + "role": "assistant", + "status": "completed", + "content": "hello world" + }, + "durable": { + "conversationKey": "snapshot-conversation", + "terminal": "completed", + "text": "hello world" + }, + "projection": { + "statuses": ["streaming", "streaming", "completed"], + "chunkCount": 2, + "text": "hello world" + }, + "wire": ["chunk:hello ", "chunk:world", "completed"] +} diff --git a/docs/deepseek-harness-plugin-ui-architecture-research.md b/docs/deepseek-harness-plugin-ui-architecture-research.md new file mode 100644 index 000000000..f1021d66d --- /dev/null +++ b/docs/deepseek-harness-plugin-ui-architecture-research.md @@ -0,0 +1,389 @@ +# DeepSeek Harness 插件化 UI 与社区皮肤机制研究 + +> 研究日期:2026-08-19 +> 研究对象:本地 `C:\VCP\vchat-develop\deepseek-harness`、Harness Client UI 包、官方插件开发规范,以及公开可见的主题/外观/Galgame 皮肤插件仓库。 +> 研究目的:解释 Harness 的“极端组件化”到底是什么,社区插件如何增强原 UI,并判断哪些机制适合 VCPChat。 +> 当前项目事实:[`next-ui-current-state.md`](./next-ui-current-state.md)。 + +## 一、结论先行 + +DeepSeek Harness 的 UI 确实高度组件化,但它的核心不是“把所有 HTML 都换成 React 组件”,而是把 UI 能力拆成可组合、可定位、可撤销的 Client 插件: + +```text +Host Plugin(可没有运行时逻辑) + ↓ 组合/授权 +Client Plugin + ├── 订阅一个明确 Slot + ├── 使用 Theme Service 的语义 token + ├── 只渲染自己的局部组件 + ├── 通过 Host Call/标准 props 取得所需数据 + └── 由 Cordis effect 自动撤销 +``` + +它不鼓励插件直接接管 `document.body`、全局 `window` 或硬编码产品 DOM selector。正式扩展应该进入 Slot、Theme Service 或 Package 自己的局部样式;只有皮肤层为了实现整套视觉状态,才会在明确范围内观察稳定的产品 DOM,并把所有写入和 Observer 收回。 + +社区插件大致分为三类: + +1. **Token/Slot 原生扩展**:例如外观插件通过 `ctx.theme.overrideTokens()`、`settings.general.item` 和本地持久化增加主题设置,卸载后恢复默认。 +2. **局部皮肤叠加**:例如 Afterglow 只增加自己的角色立绘、HUD、名牌和 CSS,并通过真实 DOM 状态判断对话/执行/错误,不伪造进度。 +3. **工作区/Agent 辅助项目**:例如 Galgame 工具把角色卡、RAG、图片生成和人格切换接成工作流,不一定直接替换 Harness 主 UI。 + +对 VCPChat 的启示是:应把 VCPUI 继续发展为“稳定的 UI 扩展平台”,但要同时提供主题 token、Surface Slot、状态选择器、owner scope 和卸载合同。VCPChat 当前已经有 `VCPUI`、`LifecycleScope`、`apps/commands Registry` 和嵌入式 App;缺的是一套面向第三方皮肤的正式扩展面。不能让插件只能靠 `styles.css + querySelector + MutationObserver` 猜测主窗口结构。 + +## 二、Harness 的 UI 架构到底是什么 + +### 2.1 Web 入口极薄,实际 UI 在 Client 包 + +本地 Harness 的 `apps/web/src/main.ts` 只负责找到 `#root` 并启动 `AppWebEntry`。Loader、模块表、AppRoot、插件组合和客户端 UI 不在入口文件里展开。 + +实际 UI 分布在 `packages/client/ui-*`: + +- `ui-conversation`:聊天、消息、输入、队列、分支、工具状态; +- `ui-sidebar`、`ui-layout`:布局和侧栏; +- `ui-settings-*`:设置领域和具体设置区; +- `ui-theme`:主题 token、启动主题和外观设置; +- `ui-slots`:插槽声明、查询和注册; +- `ui-tool`、`ui-plan`、`ui-goal`、`ui-subagent`:业务能力对应的局部 UI; +- `web-react`:Client React 运行时桥接。 + +这是一种“按能力和领域拆包”的组件化,而不是按视觉原子拆成几百个无业务归属的组件。一个 `ui-conversation` 包可以内部包含多个 React component、store、状态机和 CSS;外部只依赖它公开的 Slot、props、事件和服务。 + +### 2.2 Slot 是扩展点,不是任意 DOM 注入 + +插件开发规范要求先查询 Slot 树,再注册最窄的入口: + +```text +settings.general.item +sidebar.footer.action +conversation.chat.turnTail +tool.view.cordis +shell.overlay +``` + +Slot 会声明自己的目的、拓扑、注册协议(single/list/keyed/chain)、props 和当前 occupant。插件注册 disposer,停止或更新时自动移除。 + +这带来三个重要性质: + +1. 插件不需要猜测“某个按钮现在在 DOM 哪个位置”; +2. 多个插件可以在同一个区域并存、排序或按 key 替换; +3. 产品主 UI 改布局时,只要 Slot 合同稳定,插件不必跟着改 selector。 + +Slot 不是“允许插件重写整个页面”的通行证。规范明确提醒:不要默认注册 root/sidebar/conversation 等大 Slot,因为替换一个顶层 occupant 可能连同其 descendant Slots 一起删除。 + +### 2.3 Theme Service 与局部样式分工 + +Harness 把主题分成两层: + +- **全局主题**:先查询 Theme token,再通过 `theme.overrideTokens()` 覆盖语义变量,并保存 disposer; +- **插件局部组件**:使用 `styles.insert(css)` 或 CSS Module,颜色优先引用主题变量,不改全局产品 DOM。 + +主题服务只改变 token,不创建 UI;Slot 只创建 UI,不替代主题系统。这是一个很重要的分工,避免“一个皮肤插件既创建巨大 DOM,又重写所有主题变量,又接管业务行为”。 + +公开的 `dsh-ui-appearance` 是一个典型例子: + +- 通过 `settings.general.item` 注册“个性化外观”设置行; +- 颜色角色通过 `ctx.theme.overrideTokens()` 覆盖语义 token; +- 壁纸使用自己拥有的 fixed layer,位于 `#root` 之下; +- 模糊应用在壁纸 layer,而不是 `#root` 的 `backdrop-filter`,避免改变 fixed tooltip、toast 和菜单的 containing block; +- 设置存入插件自己的 `localStorage` 命名空间,并在加载时 schema 校验和钳制; +- 插件卸载时撤销 token、style、背景图层和视频 URL。 + +这个设计比“把一张背景图塞到 body,再给所有元素加半透明”稳定得多,因为它明确了层级、性能和撤销责任。 + +### 2.4 Host/Client 双半部 + +Harness 插件通常有 Host 半部和 Client 半部: + +- Host 负责插件组合、权限、生命周期和必要的 Host Service; +- Client 负责浏览器 UI、Slot、主题和局部状态。 + +很多纯主题插件的 Host `apply()` 是空的,这是有意的:它只需要被组合树加载,真正的主题/设置逻辑在 Client。这样可以避免为了声明一个 UI 设置而把浏览器代码、React 或主题依赖带到 Node Host。 + +这对 VCPChat 的启示不是照搬 Host/Client 包,而是区分: + +- Electron main/preload 能力; +- renderer Surface; +- 纯视觉插件; +- 需要 IPC/文件/网络的插件。 + +不需要 IPC 的皮肤不应该为了改几个 token 进入 main process,也不应取得不必要的权限。 + +## 三、社区换肤和 Galgame 插件如何增强原 UI + +### 3.1 纯主题包:生成 token,而不是逐条写组件 CSS + +`nevertoday/dsh-theme-plugin` 的做法很有代表性:49 个传统色锚点 × light/dark = 98 个主题,每个主题完整生成 token 词汇,包含文本、背景、边框、按钮、气泡和 Shiki 语法高亮,并用 3136 条对比度断言验证 WCAG AA。 + +它还把用户难以理解的“颜色参数”转成工作情绪筛选(晨起、心流、禅定、攻坚、爆肝、夜航、收工),主题选择器自己也使用同一套 token 预览。这里的关键不是主题数量,而是: + +- 主题由数据生成,不手工维护 98 份 CSS; +- 主题完整覆盖 token vocabulary,缺一项就失败; +- 对比度、色相分离、最饱和区域和派生关系都有可计算的不变式; +- light/dark 是同一主题数据的两个投影,不是两套随意 CSS。 + +VCPChat 当前的纸墨与机芯、Aero 等主题仍有“源主题文件 + 运行时生成 `styles/themes.css` + 组件特例”的多层真源风险;Harness 主题包说明我们应把主题变成可生成、可审计、可回滚的 token 数据,而不是通过 `!important` 修补视觉。 + +### 3.2 外观插件:设置 Slot + token 覆盖 + 独立背景层 + +`dsh-ui-appearance` 提供调色器、壁纸/视频、透明度、模糊、遮罩、预设、导入/导出和跨标签同步。它不是修改核心 UI,而是: + +```text +settings.general.item + ↓ +localStorage schema + ↓ +token override + owned background layer + ↓ +局部 CSS 使用 token +``` + +它特别值得 VCPChat 借鉴的技术细节: + +- 背景图层 `pointer-events: none`,不阻断产品交互; +- 图层 `inset: -48px` 给 blur 留出边缘空间,避免透明 bleed; +- 不对 `#root` 使用 `backdrop-filter`,防止固定定位菜单/Toast 被重新锚定; +- 视频对象 URL 有明确 revoke,旧视频被替换时删除 IndexedDB 记录; +- 异步视频加载有 key 对账,旧请求完成后不能把已删除视频重新挂回 DOM; +- localStorage 失败时保留当前会话状态,但不假装持久化成功。 + +这比 VCPChat 当前一些主题规则直接叠加 `backdrop-filter`、阴影和透明层更容易控制,也能解释最近聊天滚动、气泡光晕和主题重新应用问题。 + +### 3.3 Galgame 皮肤:观察真实 DOM,增加自己的 presentation layer + +`xemaya/dsh-afterglow` 是一种不同的设计:它不是改造 Harness 内部组件,而是在原 UI 上增加自己的角色 stage、名牌、战斗 HUD 和状态装饰。 + +它的核心方法: + +1. 创建完全属于皮肤的节点,并加 `data-skin-owner="afterglow"`; +2. 用 `MutationObserver` 观察少量稳定的产品事实: + - `[data-question-key]` / `[data-plan-review-key]` / `[data-approval-key]`:真实选择态; + - `[data-state='running']` 或 `[role='status']`:真实执行态; + - 工具行变为 `[data-state='error']`:真实错误态; +3. 把这些事实映射成 `dialogue / battle / choice / alert / clear` 皮肤状态; +4. 用双图片交叉淡入切换立绘,避免替换正在显示的图片造成闪烁; +5. 所有新增节点、body 属性、背景样式、favicon、title、Observer 和 timer 都由一个 effect disposer 回收; +6. 皮肤装饰节点统一 `aria-hidden="true"`,不把纯装饰伪装成业务信息; +7. `prefers-reduced-motion` 下关闭动画; +8. 不制造假的百分比或假的“执行进度”,所有状态来自产品真实 DOM。 + +这类皮肤的优点是自由度极高:可以把普通聊天变成 Galgame 对白,把执行状态变成 Boss 战 HUD,把审批变成路线选择,把 Agent 变成有表情的角色。但它的代价也很明显: + +- 依赖宿主 DOM selector; +- 宿主改名/改结构会导致皮肤失效; +- MutationObserver 可能带来性能成本; +- 皮肤状态和业务状态之间容易产生误判; +- 如果没有 disposer,换肤/热重载会重复添加节点和监听器; +- 如果插件重写产品 DOM,容易和主 UI 的焦点、ARIA、主题和测试发生冲突。 + +Afterglow 的可取之处不是“所有皮肤都应该观察 DOM”,而是它把这种做法限制在**明确的 presentation-only 层**,并且只观察稳定、可解释的状态属性。 + +### 3.4 Galgame 工作流项目:不一定是 UI 皮肤 + +`deepseek-harness-galgame` 这类项目更接近“工作区 + Agent workflow”:角色卡、设定库 RAG、人格切换、图片生成和本地绘画后端组成一条创作流水线。它可以让对话呈现 Galgame 体验,但主要改变的是 Agent 的人格、资料和工具工作流,不一定直接替换主聊天 UI。 + +这提醒我们区分三种产品变化: + +| 类型 | 改变什么 | 适合的扩展面 | +|---|---|---| +| Theme | 颜色、字体、背景、材质、密度 | Theme token / appearance settings | +| Skin | 额外角色层、HUD、名牌、状态装饰 | presentation Slot / owned overlay | +| Workflow | 角色卡、RAG、图片生成、工具和人格 | Agent/Tool/Workspace capability | + +把 Workflow 误做成 CSS 皮肤,会导致 UI 承担业务状态;把 Skin 误做成聊天业务改造,则会破坏上游语义。VCPChat 应该为三者保留不同边界。 + +## 四、这是否真的是“极端组件化” + +### 4.1 极端之处 + +Harness 把以下内容都做成独立 Client package 或可查询扩展面: + +- 聊天输入、消息行、工具行、审批、计划、目标、队列、侧栏、设置、主题; +- 每个页面扩展通过 Slot 注册; +- 每个插件的服务、timer、style、theme override 和注册都要求 disposer; +- 主题和 UI 通过 token/Slot 传递,而不是跨插件查找 DOM; +- 测试按具体 UI package、真实浏览器入口和 snapshot 场景拆分。 + +### 4.2 它并不极端到“每个元素都是插件” + +一个 UI package 内部仍然可以拥有组合组件、store、状态机和局部 CSS。拆分的依据是: + +- 是否有独立 owner; +- 是否有独立生命周期; +- 是否有独立生产消费者; +- 是否需要独立测试/发布; +- 是否需要被插件扩展。 + +如果一个按钮只被一个页面使用,它不一定要成为公共插件;如果一个设置区需要第三方扩展,就应该有 Slot;如果只是统一颜色,就应该是 token,而不是新的组件 API。 + +这正好对应 DeepSeek Harness simplification 规范:没有消费者的公共面、Registry kind、runtime 或测试 facade 应删除,而不是因为“组件化看起来先进”就保留。 + +## 五、对 VCPChat 的直接启示 + +### 5.1 建立三层可扩展 UI 合同 + +VCPChat 可以在现有 VCPUI 之上增加三个明确层次: + +```text +VCPUI Foundation + ├── Theme tokens:颜色、材质、密度、焦点、motion + ├── Stable components:Button/Input/Select/Modal/Toast... + └── App Shell:Tab、Launchpad、Overlay、内嵌 View + +VCPUI Surface Slots + ├── settings.general.item + ├── chat.composer.leading / trailing + ├── chat.message.meta / turnTail + ├── sidebar.footer.action + ├── app.overlay + └── account.menu.item + +Presentation Skin API + ├── state selectors(只读真实产品事实) + ├── owned DOM/overlay + ├── scoped style sheet + ├── theme override(可选) + └── disposer / reduced-motion / accessibility contract +``` + +第一层是当前 VCPUI;第二层是未来第三方扩展的正式入口;第三层允许 Galgame/角色皮肤存在,但不能直接修改聊天业务状态或替换整个主窗口。 + +### 5.2 先提供 Slot,不要先允许任意 DOM 注入 + +最有价值的首批 Slot 应该是“小而高频”的局部入口: + +- 主聊天输入框前后区域; +- 消息尾部操作区或真实 turn tail; +- 侧栏底部工具入口; +- Account Menu 的附加项; +- Launchpad/App 页面工具栏; +- 全局 overlay 的装饰层; +- 设置页明确的 `general.item` 或独立 section。 + +暂不开放: + +- 替换整个聊天根节点; +- 替换主 Sidebar/主 Tab Host; +- 直接改消息业务 HTML; +- 直接注册 preload/IPC 任意方法; +- 读取所有 manager 私有状态。 + +### 5.3 为皮肤提供稳定的只读状态事实 + +Afterglow 依赖 DOM selector 是因为它没有更好的公开状态面。VCPChat 可以提供更稳定的只读状态属性或事件: + +```text +data-vcp-chat-state="idle|streaming|error|waiting" +data-vcp-active-surface="chat|settings|app" +data-vcp-dialog-state="open|closing|closed" +data-vcp-tool-state="running|success|error|stopped" +data-vcp-theme="..." +``` + +皮肤只能读取这些状态,不能写入它们。状态由 ChatManager、OverlayCoordinator、AppTabHost 和 Theme engine 负责发布。这样皮肤不会依赖 `.some-old-class-name`,也不会自己猜测“是不是在运行”。 + +### 5.4 皮肤的生命周期合同 + +一个 VCPChat 皮肤正式启用前至少必须提供: + +- `mount()` / `dispose()`; +- 自己的 root 标识和所有节点 owner; +- 自己的 style sheet 和 token override disposer; +- Observer、timer、Promise 和 object URL 清理; +- theme/light-dark 变化同步; +- `prefers-reduced-motion`; +- 装饰节点 `aria-hidden`,交互节点拥有完整 ARIA; +- 不覆盖业务焦点、不劫持 Escape、不伪造进度; +- renderer reload/crash 后可重新挂载或安全失效。 + +### 5.5 主题系统应借鉴生成式 token + +对 VCPChat 来说,最现实的改进不是立刻支持 98 个主题,而是: + +1. 定义完整且稳定的 semantic token vocabulary; +2. 主题源数据生成 light/dark token; +3. 自动检查对比度、focus ring、disabled、selection、代码高亮、气泡和滚动条; +4. 禁止主题源与实际加载产物漂移; +5. 允许插件只覆盖明确的 token namespace; +6. 卸载插件后 token 完整恢复。 + +### 5.6 背景/毛玻璃要采用独立 Layer,而不是污染 root + +`dsh-ui-appearance` 对 VCPChat 的直接启发: + +- 壁纸用独立 fixed layer,`pointer-events: none`; +- blur 应用在壁纸 layer,而不是根容器; +- 避免 `backdrop-filter` 改变 fixed 菜单、Toast、Tooltip 的 containing block; +- 透明度和遮罩成为 token/变量,不要每个组件自己调 alpha; +- 视频/图片异步资源拥有 object URL、缓存和失败回退; +- 主题切换要用 key/token 对账,旧异步结果不能重新插回 DOM。 + +这可以直接降低 VCPChat 近期出现的启动错误首帧、气泡光晕、快速滚动半黑和主题重新应用后才恢复的问题。 + +## 六、哪些社区做法不能直接照搬 + +| 做法 | 看起来很灵活 | 真实风险 | VCPChat 处理 | +|---|---|---|---| +| 全局 CSS 覆盖所有 `.message-item` | 快速换皮 | 主题/上游改版时冲突,`!important` 失控 | 只允许 token 和 scoped Surface CSS | +| MutationObserver 观察整棵 body | 任何状态都能感知 | 性能、重复挂载、误判和 selector 脆弱 | 只观察稳定状态根或正式事件 | +| 替换主聊天 DOM | 视觉自由度最高 | 复制业务状态、破坏插件和消息语义 | 只做 presentation Slot/装饰层 | +| 皮肤自建一套 loading/progress | 视觉更像游戏 | 假进度、业务终态不一致 | 只映射真实 running/error/approval 状态 | +| 主题直接写 body/root backdrop-filter | 简单 | fixed overlay、Toast、菜单定位改变 | 独立 background layer | +| 把主题设置写进核心 settings | 看似统一 | 第三方设置 schema/权限耦合 | 插件 namespace + schema 校验 + disposer | +| 将 Galgame 角色卡和 UI 混为一体 | 体验连贯 | 业务人格、UI 状态和资源生命周期耦合 | Workflow、Skin、Theme 分层 | + +## 七、建议的 VCPChat 落地路线 + +### B0:扩展面事实审计 + +- 列出当前 VCPUI、apps/commands Registry、settings bridge、Theme engine、Overlay、AppTabHost 的可扩展入口; +- 标记哪些是生产合同、哪些仅是内部实现、哪些是用户自定义的潜在需求; +- 搜索现有插件是否直接依赖私有 DOM、`!important` 或全局 CSS; +- 退出条件:没有未定义 owner 的“默认扩展入口”。 + +### B1:Theme Token 与资源撤销合同 + +- 收口主题源/产物,定义 token namespace 和插件 override owner; +- 提供背景 layer、图片/视频、模糊、遮罩、字体和 icon 的统一资源撤销; +- 退出条件:主题/外观插件可以安装、实时应用、重载、卸载并恢复默认,且没有 detached style/layer/object URL。 + +### B2:首批 Surface Slots + +- 先开放聊天 composer、message tail、sidebar footer、Account Menu item、settings general item、app toolbar/overlay; +- 每个 Slot 明确 single/list/keyed/chain 协议、排序、props、owner、dispose 和权限; +- 不开放任意 DOM selector 和全局 root replacement; +- 退出条件:至少一个真实第三方式插件能注册、使用、卸载,产品原 UI 与其他插件不受影响。 + +### B3:Presentation Skin API + +- 定义只读状态属性/事件; +- 提供 scoped skin root、装饰 overlay、theme token override、reduced-motion 和 dispose 合同; +- 建立一个最小“状态皮肤”示例,只展示真实 streaming/error/approval 状态,不伪造进度; +- 退出条件:皮肤可在聊天、设置和内嵌 App 之间切换,renderer reload/crash 后不重复挂载,所有装饰节点可归属和清理。 + +### B4:Galgame/角色化体验试验 + +- 以独立可卸载皮肤验证角色立绘、名牌、对话纸、状态 HUD、审批选择等表现; +- 角色卡、RAG、图片生成和人格切换仍属于 Workflow/Agent 插件,不塞进 Skin API; +- 交互元素必须使用真实 Slot 和业务事件,装饰元素 `aria-hidden`; +- 退出条件:主题、皮肤、无皮肤三种状态可逆,键盘、Escape、焦点和业务消息语义不退化。 + +### B5:市场/分发与安全边界 + +- 插件 manifest 声明 Host/Client 权限、Slot、token namespace、资源大小和版本; +- 安装/更新/卸载有明确状态和失败回滚; +- 禁止第三方皮肤任意读取敏感聊天内容、调用未声明 IPC 或注入外部网络资源; +- 退出条件:恶意/损坏/旧版本插件不会破坏主 UI,卸载能恢复所有产品资源。 + +## 八、最终判断 + +DeepSeek Harness 的“极端组件化”值得借鉴的是**扩展点和所有权的精确化**,不是无条件拆分或 React 化。它允许社区把同一套聊天产品变成传统色主题、毛玻璃外观、Galgame 对话或战术 HUD,是因为核心 UI 已经把“数据/状态、Slot、Theme、局部组件、Host 权限、生命周期”分开。 + +VCPChat 现在已经完成了内部架构收口,但还没有把这套能力正式开放给皮肤生态。最稳妥的下一步是先做 B0–B2:收口主题 token,提供少量高价值 Surface Slot,建立安装/卸载和 disposer 合同;之后再做 B3–B4 的 Galgame/角色化试验。不要先允许任意 CSS/DOM 注入,否则会把当前正在解决的两套 UI、主题漂移、焦点误关和资源泄漏重新引回来。 + +## 参考证据 + +- 本地 Harness:`C:\VCP\vchat-develop\deepseek-harness\apps\web\src\main.ts`、`packages/client/ui-*`、`apps/cli/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md`。 +- 主题插件:[`nevertoday/dsh-theme-plugin`](https://github.com/nevertoday/dsh-theme-plugin)。 +- 外观插件:[`TQSY114514/dsh-ui-appearance`](https://github.com/TQSY114514/dsh-ui-appearance)。 +- Galgame/工作流项目:[`FynnReinhardt/deepseek-harness-galgame`](https://github.com/FynnReinhardt/deepseek-harness-galgame)。 +- Presentation skin:[`xemaya/dsh-afterglow`](https://github.com/xemaya/dsh-afterglow)。 diff --git a/docs/deepseek-harness-ui-ux-research.md b/docs/deepseek-harness-ui-ux-research.md index 8de416285..22d0e07f3 100644 --- a/docs/deepseek-harness-ui-ux-research.md +++ b/docs/deepseek-harness-ui-ux-research.md @@ -1,7 +1,7 @@ # DeepSeek Harness 对 VCPChat UI/UX 的启发研究 -> 研究日期:2026-08-19 -> 研究对象:`C:\VCP\vchat-develop\deepseek-harness` 的 Agent/工程规范,以及 VCPChat 当前主窗口、VCPUI、内嵌应用、主题和 Electron 交付体系 +> 研究日期:2026-08-19 +> 研究对象:`C:\VCP\vchat-develop\deepseek-harness` 的 Agent/工程规范,以及 VCPChat 当前主窗口、VCPUI、内嵌应用、主题和 Electron 交付体系 > 结论性质:设计与工程决策参考,不自动成为施工计划。具体实现以 [`next-ui-current-state.md`](./next-ui-current-state.md) 和 [`next-ui-development-roadmap.md`](./next-ui-development-roadmap.md) 为准。 ## 一、结论先行 diff --git a/docs/next-ui-current-state.md b/docs/next-ui-current-state.md index ac4ff1497..a1f058b4f 100644 --- a/docs/next-ui-current-state.md +++ b/docs/next-ui-current-state.md @@ -1,10 +1,12 @@ -# Next UI 当前架构与交付基线 +# 当前 UI 架构与交付基线 > 状态:当前权威事实文档
-> 核对日期:2026-08-19
-> 核对范围:`codex/ui-ux-harness-research-20260819` 已提交文件树相对 `upstream/main`
+> 核对日期:2026-08-21
+> 核对范围:当前工作树 `codex/chat-kernel-deep-decoupling-draft-20260821` 的实际代码、门禁与测试结果
> 后续施工顺序:[`next-ui-development-roadmap.md`](./next-ui-development-roadmap.md) +> 文件名中的 `next-ui` 是历史命名。当前主窗口只有一套正式布局;`next` 是现行主窗口 presentation,不再与 Classic 构成可切换的双布局。Classic 仅保留给未迁移的业务子页面和兼容边界。 + ## 1. 文档用途 本文只回答四个问题:当前代码真实运行成什么样、哪些能力已有生产消费者、哪些能力只有测试或没有消费者、当前是否达到上游 PR 门槛。它不保存迁移过程,也不把计划中的能力写成已经交付。 @@ -51,24 +53,8 @@ | Web Awesome 离线闭包 | 已完成 | 固定版本、101 文件 closure、vendor 与 pack check | | 前端插件兼容边界 | 已完成 | Loader 恢复上游合同;Next 生命周期不接管插件运行时 | | 上游消息组件视觉语义 | 已保护 | Next 不重绘结构化消息内部组件,边界门禁存在 | -| Escape 与键盘导航所有权 | 已完成核心范围 | 优先级 Escape dispatcher、Launchpad 方向键/Home/End、controller fallback listener dispose 回归 | -| Launchpad 隐藏状态可访问性 | 已完成核心范围 | 关闭时 AppTabHost 设置 `aria-hidden` 与 `inert`,防止动态应用按钮获得焦点;打开/关闭回归覆盖 | -| 应用托盘抽屉隐藏状态可访问性 | 已完成核心范围 | 关闭/出场动画期间 trayManager 同步 `aria-hidden` 与 `inert`;Electron smoke 验证打开和关闭终态 | -| 通知菜单单一所有者 | 已完成 | 旧 `event-listeners` 重复绑定已停用;controller 单测与 Electron 命令计数证明每个动作只执行一次 | -| 全局设置永久 pending | 已完成核心范围 | 保存 IPC 有界等待,超时释放提交锁并进入可恢复失败;新增回归覆盖 | -| 异步 busy 状态语义 | 已完成核心范围 | Ask Nova composer 与主聊天流式发送按钮同步 `aria-busy`,成功/取消后由真实终态清除;Electron 主聊天序列与 UI contract 覆盖 | -| Overlay teardown 与原生 View 对账 | 已完成核心范围 | OverlayCoordinator dispose 清空 lease 后主动 reconcile,避免隐藏 WebContentsView 遗留;dispose 回归覆盖 | -| 动态标签关闭焦点恢复 | 已完成核心范围 | AppTabHost 关闭当前 tab 时将焦点恢复到相邻 tab 或主聊天 Home;真实 tab controller 回归覆盖 | -| Ask Nova 与聊天模式交互合同 | 已完成核心范围 | Ask Nova tabs 使用 roving tabindex/方向键/tabpanel 关系;聊天模式浮层同步 `aria-hidden`/`inert`,保存按最新意图串行化 | -| Native Modal 可访问名称 | 已完成核心范围 | VCPUI native dialog 生成稳定 `aria-labelledby` 与标题节点;contract test 覆盖 | -| 兼容确认框模态语义 | 已完成核心范围 | 上游 `showConfirmDialog` 保留原 API,但补齐同步 active 状态、dialog/aria-modal、焦点陷阱、Escape 捕获和焦点恢复 | -| 动态应用标签与设置导航可访问性 | 已完成核心范围 | 动态标签使用独立 tab button、关闭 sibling 与 labelled tabpanel;全局设置分类使用垂直 tablist、方向键/Home/End 与 section 关系 | -| 应用托盘设置弹窗 | 已完成核心范围 | 保留上游托盘 API,补齐 dialog 语义、焦点陷阱、Escape owner 与迟到 overlay acquire 的安全释放 | -| 设置 Electron 证据 | 已完成当前 canonical 范围 | 真实保存、reload 恢复、搜索/分类切换与主题截图通过;旧的 Classic teardown 断言已移除,避免把已合并主 Surface 当成双布局契约 | -| 高频主题切换证据 | 已完成当前 canonical 范围 | 真实 Electron smoke 连续切换 20 次,最终主题、Web Awesome theme link 数量和 owner 数量均与切换前一致 | -| macOS unpacked packaged launch | 证据未完成 | 本机已生成 `dist/mac-arm64/VCP聊天客户端.app` 并确认 `app.asar`/资源存在;直接启动未建立 remote-debugging endpoint,未将未签名产物启动结果计为通过 | - -历史证据快照(2026-08-17,P3,不代表当前 HEAD 的 Windows 或发布证据):UI System 74/74、Electron UI Apps 22/22、24 步主聊天序列通过;生命周期压力测试 3 次预热加 20 次测量后保持 407 个 listener、8 个 Scope、162 项受管资源和 5 个 Electron process,detached root/icon/option 为 0。该结果证明当时覆盖路径稳定,不代表任意服务、GPU、休眠或第三方插件组合绝对无缺陷。 + +历史完整证据基线(2026-08-17,P3):UI System 74/74、Electron UI Apps 22/22、24 步主聊天序列通过;生命周期压力测试 3 次预热加 20 次测量后保持 407 个 listener、8 个 Scope、162 项受管资源和 5 个 Electron process,detached root/icon/option 为 0。该结果只证明当时已覆盖路径稳定,不代表当前分支或任意服务、GPU、休眠、第三方插件组合绝对无缺陷。 ## 4. 公共能力收口状态 @@ -102,41 +88,53 @@ P2 已删除休眠子页面 Next runtime、mode 传播和测试专用 settlement ## 6. 当前 PR 就绪判断 -当前分支已完成 P0–P3 与 P4 自动化门禁。分支基线最后一次施工同步为 `upstream/main` `3da77f00`;随后已只读审计当前 `upstream/main` `94923696` 的 7 个新提交:其中聊天气泡边界提交 `fabcbbd1` 已由本分支既有 `14b47e74` 等价实现覆盖(`styles/chat.css` 哈希一致),无需重复合并;VCPMobileSync/Rust 更新属于独立上游范围,暂不合并以避免扩大本路线和插件/数据服务边界。两处共享文件已逐段审查并以理由和规范化文本哈希更新冻结基线。 +当前 UI 拓扑已收敛为单一主窗口 presentation。Chat Kernel 深度解耦的 D5/D6 已在范围内通过:`renderer.js` 只承担 composition/lifecycle orchestration,本路线指定的 settings/filter/presentation ambient facade 已退休。D7 仍未完成,原因是缺少跨 Windows/打包/GPU-DPI 矩阵和 30–60 分钟人工 soak;不得将单主机自动化外推为最终发布就绪。D 阶段状态以 [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md) 为唯一权威,测试数字只以该审计及其记录的最近一次执行为准。 + +以下 P0–P4、同步提交和历史 Windows/macOS 数字均为 2026-08-17 的阶段性验收记录,保留用于追溯,不代表当前分支的最新通过状态。 Windows 生成的 `settingsManager.js` 基线曾错误绑定 CRLF 工作区字节;门禁现统一按 LF 文本语义计算 SHA-256,并用 LF/CRLF 等价断言防复发。Web Awesome 生成产物通过 `.gitattributes` 仅在 `vendor/webawesome-runtime/**` 禁用 text conversion 和 whitespace diagnostics;源码检查保持启用,pack check 会验证该属性没有丢失。 -同步后的 macOS 自动证据: +当前分支自动证据(详细阶段状态以最终审计为准): -- `npm run check:ui-system`:当前 HEAD 首次运行因新增外部证据清单未加入设计边界白名单而失败;该守卫同步修复后需重新取得绿证据,不能引用旧快照代替。 -- Electron UI Apps:22/22。 -- 主聊天操作序列:24 actions、11 action kinds、21 pairs、12 transitions、2 faults、required edge 1/1。 -- 生命周期压力:3 次预热 + 20 次测量;861 listener、8 Scope、162 受管资源、5 process、2 renderer process 在全部 checkpoint 恒定,detached root/icon/option 为 0,heap 约 9.8 MiB → 9.7 MiB。 -- Web Awesome closure:101 files、0.46 MiB,可重复生成;pack check 通过。 -- `git diff --check upstream/main...HEAD`:通过。 +- Chat Kernel:146/146;UI System:97/97。 +- `npm run check:ui-system`:通过,包含 design subtraction、consumer、Classic、Next、应用运行时和 UI System 全链路。 +- Electron UI Apps:24/24;主聊天与辅助窗口证据、当前主机 Windows 矩阵见 [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md)。 +- 当前工作树 Windows matrix:六行全部通过,包含 60-cycle lifecycle;30 分钟 manual-soak 观察已生成 artifact,但 checklist 仍为 `manual_observation_required`。 +- Electron unpacked packaged 构建当前因 `electron-edge-js` MSBuild exit 1 失败;不能把 source/runtime smoke 外推为 packaged PASS。 +- Web Awesome closure 与 pack check:沿用既有通过证据,发布配置矩阵仍属于 D7 未完成项。 -尚未完成的发布证据包括当前上游增量重新归因、Windows 复验、签名/可见桌面环境中的 packaged launch 和 30–60 分钟人工 soak。二者不应由 macOS 自动结果代替,因此当前状态是“代码与自动门禁就绪,跨平台/打包/人工发布证据待补”。本分支明确不携带 `.github/workflows/**`,跨平台验证由外部 Windows 环境或上游 CI 执行。 +尚未闭合的发布证据仍包括 30–60 分钟人工 soak、真实 packaged Electron runtime 和跨配置矩阵;`npm run check:chat-release-evidence` 会在这些证据缺失时明确失败。这不影响 C0–C7 实现退出条件,但不应把自动矩阵代替人工体验观察。本分支现提供 Chat Kernel 与 UI 的 PR 门禁工作流;跨配置 Windows、打包安装和人工 soak 仍需单独执行。 -2026-08-19 Harness 路线最新自动证据:`test:ui-system` 86/86,Electron UI Apps 22/22,主聊天序列 24 actions / 11 action kinds / 21 pairs / 12 transitions / 2 faults,生命周期压力 3 次预热加 20 次测量后保持 873 listener、8 Scope、174 项受管资源和 5 个 Electron process,detached root/icon/option 为 0,heap 约 10.2 MiB → 10.1 MiB;Web Awesome 101 文件 closure 与 pack check 通过。这些数字是该次 macOS 运行证据,不替代 Windows 与人工 soak。 +当前文档权威关系已在 2026-08-20 收敛;历史文档可以保留当时的双 presentation 描述,但不得用于描述当前主窗口拓扑。 -本机 `npm run pack` 能生成 macOS arm64 目录。使用隔离 `userData` 和 `VCPCHAT_E2E_TEST=1` 启动打包 app 后,进程可存活但 15 秒内未创建 remote-debugging 端口;`sample` 显示主线程停在原生 `NSAlert runModal`,因此“打包产物实际启动”仍是未通过的 A6 证据。该结果需要在签名/可见桌面环境进一步定位,不被 repo closure 检查替代。 +P1 所有权缺陷已于 2026-08-17 关闭:组件展示页不再调用全局 `cancelAll()`,其 Feedback 与 timer 均由页面 Scope 持有。Windows 验证为 UI System 75/75、Electron UI Apps 22/22、生命周期压力 3 次预热 + 20 次测量通过;压力 checkpoint 保持 8 个 Scope、164 项受管资源、410 个 listener、5 个 Electron process,detached root/icon/option 为 0。 -当前文档权威关系已在 2026-08-17 收敛;历史文档可以保留当时的双 presentation 描述,但已明确标记为历史记录。 +P2 无消费者架构减法已于 2026-08-17 关闭:产品文件树不再携带休眠子页面 runtime、静态 mode facade 或 Settings/Creation/item list 测试 Store;负向边界门禁阻止这些接口在无生产消费者时回归。完整 Windows 验证为 UI System 75/75、Electron UI Apps 22/22、24 步主聊天序列和生命周期压力 3 次预热 + 20 次测量通过。 -历史证据快照(2026-08-17,不代表当前 HEAD 的 Windows 证据):P1 所有权缺陷已关闭;组件展示页不再调用全局 `cancelAll()`,其 Feedback 与 timer 均由页面 Scope 持有。此前记录的 UI System 75/75、Electron UI Apps 22/22 和生命周期压力结果只作为当时的回归记录。 +P3 公共合同收口已于 2026-08-17 关闭:13 个 Stable 组件均具备生产与 Electron 证据,19 个 Candidate 继续用于正式组件库展示;Registry 仅保留 `commands/apps`,内部应用注销会关闭对应 tab 与 Surface。完整 Windows 验证为 UI System 74/74、Electron UI Apps 22/22、24 步主聊天序列和生命周期压力 3 次预热 + 20 次测量通过;压力 checkpoint 保持 8 个 Scope、162 项受管资源、407 个 listener、5 个 Electron process,detached root/icon/option 为 0。 -历史证据快照(2026-08-17,不代表当前 HEAD 的 Windows 证据):P2 无消费者架构减法已关闭;产品文件树不再携带休眠子页面 runtime、静态 mode facade 或 Settings/Creation/item list 测试 Store;负向边界门禁阻止这些接口在无生产消费者时回归。 +P4 自动门禁已关闭;同步后 Windows 复验和人工 soak 仍待补齐。动态壁纸、插件运行时和业务子页面迁移继续排除在本轮之外。 -历史证据快照(2026-08-17,不代表当前 HEAD 的 Windows 证据):P3 公共合同收口已关闭:13 个 Stable 组件均具备生产与 Electron 证据,19 个 Candidate 继续用于正式组件库展示;Registry 仅保留 `commands/apps`,内部应用注销会关闭对应 tab 与 Surface。 +Chat Kernel 的早期 C0–C7 子路线已有既有证据;当前工作转入 D0–D7 深度解耦路线。不要用早期 C0–C7 的通过记录替代 D5–D7 的退出证据。 -P4 自动门禁已关闭;同步后 Windows 复验和人工 soak 仍待补齐。动态壁纸、插件运行时和业务子页面迁移继续排除在本轮之外。 +深度解耦已完成 D0–D6 的范围内退出:`MainChatSurfaceAdapter`、`StreamCoordinator`、`StreamTransientHistory`、operation-scoped `StreamProjection`、独立 Surface consumer 和 owner-scoped preload subscription 已接入生产;主题、设置/presentation、TTS、Flowlock、辅助 preload 事件、forward、附件和 DOM listener 均已有 named owner。D7 继续只补齐发布配置和人工观察证据,不以重写 UI 或增加 facade 为目标。 + +D0 consumer baseline 已接入 `check:ui-system`:生产引用、测试引用和 Kernel 禁止反向依赖均由 `guard:chat-kernel-consumers` 重复检查。该报告只记录当前事实,不把 legacy facade 自动升级为稳定公共 API。 + +D0–D6 已接入生产并具备各自的源码、静态门禁和自动化证据;D7 BLOCKED。历史复核的 115/115、129/129、85/85 等数字保留为历史记录,不能作为当前状态。当前 Chat Kernel、UI System、Electron 和 Windows 单主机证据统一记录在最终审计;多版本/打包/GPU-DPI 配置、packaged native rebuild 和人工 checklist 仍未完成。 + +最新 vD5 增量已将主聊天选择、history 与 TTS 状态改为显式 capability/owner:`VCPMainChatState` 只读快照替代可变 selection globals,主 history 由 `MainChatStateAuthority` 持有,`TtsSurfaceOwner` 持有 AudioContext、队列和订阅;`TopicSelectionReadiness` 替代 renderer-ready/pending selection 全局字段。Flowlock、AutoTTS 和 Electron 序列测试均已迁移到真实 consumer。该增量当时为 Chat Kernel 113/113;当前测试数字只读取最终审计。辅助 Voice/Rust 并发流场景已纳入主聊天序列。 + +并发流的终态持久化现在会在 stream terminal 显式 flush 并等待当前 topic 的 ChatRepository 保存;独立交互 Surface 的测试也在下一次请求前等待真实 `aria-busy=false` 终态,避免把 terminal 事件误当成发送 Promise 已完成。 ## 7. 文档权威关系 | 文档 | 定位 | |---|---| -| 本文 | 当前实现、真实消费者、完成度与 PR 状态的唯一权威 | -| `next-ui-development-roadmap.md` | 从当前状态继续施工的唯一权威顺序 | +| 本文 | 当前主窗口拓扑和 UI 产品状态的权威 | +| `chat-kernel-vd7-final-audit.md` | D0–D7 阶段状态、自动化证据与 D7 未完成项的唯一权威 | +| `chat-kernel-deep-decoupling-roadmap.md` | D0–D7 仍有效的目标、行为合同、架构决策和退出条件 | +| `next-ui-development-roadmap.md` | UI 后续施工顺序;不重复维护 D 阶段状态 | | `next-ui-lifecycle-architecture.md` | 生命周期合同和所有权规则 | | `main-chat-operation-sequence-testing.md` | 操作序列模型、覆盖与故障注入规则 | | `ui-engineering-standard.md` | 新代码的工程 Definition of Done | @@ -144,6 +142,7 @@ P4 自动门禁已关闭;同步后 Windows 复验和人工 soak 仍待补齐 | `classic-retirement-architecture.md` | 已完成的主窗口收敛决策与历史施工记录 | | `design-system-upstream-pr-convergence.md` | 历次 PR 审查和整改日志,不代表当前状态 | | `upstream-function-parity.md` | 历史双 presentation 验证记录,不再定义当前产品拓扑 | +| `archive/2026-08-chat-kernel-and-ui-roadmaps/` | 已完成、已停止或按时间追加的历史路线;只用于追溯,不声明当前状态 | ## 8. 更新规则 @@ -152,3 +151,8 @@ P4 自动门禁已关闭;同步后 Windows 复验和人工 soak 仍待补齐 - 任何阶段只有全部退出条件满足后才能标为完成,不能用“按需完成”替代状态。 - 新发现的问题先归因于上游或 Next delta,再决定是否进入本路线。 - 路线变化必须保留删除项和非目标,防止后续会话重新扩张范围。 +历史证据保留其原始日期和计数;当前 D5/D6 结论与 D7 阻断项只读取最终审计。完整 Windows 配置矩阵和人工 soak 仍待完成。 + +人工 soak 入口现为 `npm run test:manual-soak`,运行产物写入 `artifacts/manual-soak/` 并固定标记为 `manual_observation_required`;它只提供真实 Electron 采样和人工检查清单,不会替代人工交互或把单台 Windows 观察升级为发布证据。 + +最新单机 Windows 串行矩阵 artifact 只在 [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md) 维护;resize CDP 仍为显式 skipped,矩阵不代表多版本或安装包覆盖。 diff --git a/docs/next-ui-development-roadmap.md b/docs/next-ui-development-roadmap.md index f50d85755..a0dc77be3 100644 --- a/docs/next-ui-development-roadmap.md +++ b/docs/next-ui-development-roadmap.md @@ -44,6 +44,8 @@ | P6 按业务逐页演进 | 条件式远期 | 只在真实需求出现时迁移一个子页面 | 页面独立 PR,consumer/runtime/test 同时进入 | | 交互与可访问性行为合同 | 规划中 | 将键盘/焦点/ARIA、Modal/Select、异步终态、主题/DPI/fallback 和任务级视觉回归纳入真实 Electron 证据 | [`ui-interaction-accessibility-roadmap.md`](./ui-interaction-accessibility-roadmap.md) 的 A0–A6 全部退出;A7 仍按真实需求条件式执行 | +| Chat Kernel 与受控 UI 插件化 | 已完成(C0–C7,2026-08-19) | 将高度耦合的聊天运行时逐步拆为 Domain、Content Runtime、Renderer Adapter 与 Surface;以 Harness 消费者证据约束主题、slot 和 presentation skin 扩展 | C0–C7 路线已移入 [`archive/2026-08-chat-kernel-and-ui-roadmaps/`](./archive/2026-08-chat-kernel-and-ui-roadmaps/);后续扩展必须重新提供同等级证据 | +| Chat Kernel 深度解耦 | D5/D6 PASS,D7 BLOCKED | 将 StreamManager 的实时协调与 DOM/持久化分离,并把 `renderer.js` 收敛为主窗口 composition adapter;不重写消息协议、不迁移业务子页面 | [`chat-kernel-vd7-final-audit.md`](./chat-kernel-vd7-final-audit.md) 是阶段状态权威;D7 需跨 Windows/打包/GPU-DPI 矩阵及人工 soak,满足前不得宣称最终解耦完成 | P0–P4 是当前上游 PR 的实际路线。P5–P6 不阻塞当前施工,也不得提前把实现放入本 PR。 @@ -184,7 +186,7 @@ await feedback.dispose(); ## 8. P4:PR 证据与交付 -> 状态(2026-08-20):分支最近一次施工同步基于 `upstream/main` `3da77f00`;当前远程 `upstream/main` 已到 `94923696`,新增提交已完成只读归因。聊天气泡边界提交 `fabcbbd1` 已确认由本分支既有 `14b47e74` 等价覆盖;VCPMobileSync/Rust 更新不纳入本路线。macOS 上 UI System、Electron smoke、主聊天序列、20 轮生命周期压力、离线 closure、pack check 与完整 diff check 全部通过;剩余发布证据为 Windows 复验、签名 packaged launch 和 30–60 分钟人工 soak,完成前不标记 P4 全部完成。 +> 状态(2026-08-17):已同步 `upstream/main` `a9b36d8d`,macOS 上 UI System、Electron smoke、主聊天序列、20 轮生命周期压力、离线 closure、pack check 与完整 diff check 全部通过。CRLF/LF 冻结基线和生成 vendor whitespace 策略已跨平台化。剩余发布证据为同步后的 Windows 复验和 30–60 分钟人工 soak;完成前不标记 P4 全部完成。 ### 8.1 每次提交最小检查 diff --git a/docs/stream-switch-tool-wait-recovery.md b/docs/stream-switch-tool-wait-recovery.md new file mode 100644 index 000000000..64dafee44 --- /dev/null +++ b/docs/stream-switch-tool-wait-recovery.md @@ -0,0 +1,28 @@ +# 流式工具等待期间切换话题:保守修复记录 + +更新日期:2026-08-22 + +## 问题与边界 + +当同一条流在等待 VCP 工具回执时,用户切换到另一话题,再切回原话题,原 renderer Surface 可能已经清空 DOM/history projection;流事实仍在,但没有重新投影,因此消息看起来消失。该修复只覆盖同一 renderer 生命周期内的切换,不承诺 renderer 崩溃或进程重启后的未终结流恢复。 + +## 已落地的最小方案 + +- `StreamProjection` 为活动流保存 conversation identity、message model、operation、phase 和累计文本快照。 +- 新增 `snapshotConversation(identity)` 与 `reconcileConversation(identity)`,切回 Surface 时重新补齐 pending message 并重绘当前累计文本。 +- 快照是 renderer-transient,不写磁盘;terminal/persistence authority 不变。 +- `startStreamingMessage` 仅在已有 DOM owner 时短路,允许切回后重建 DOM。 +- identity 同时包含 `itemType`、`itemId`、`topicId`,不使用 topic 单字段猜测归属。 + +## 证据 + +- `tests/stream-manager-terminal-cleanup.test.js` 覆盖:工具等待、切换、清空 projection、切回、继续 chunk,验证 history 与 DOM 均恢复。 +- `tests/chat-manager-selection-race.test.js` 覆盖:history 文件读取期间 terminal 完成,验证文件同步不会把刚提交的 assistant 当作删除项。 +- 文件同步会保护读取开始时和读取完成时的 active stream identity,避免 terminal 在异步读期间改变 active 状态造成误删。 +- 仍需运行 Chat Kernel/UI 全量、静态门禁和真实 Electron 主聊天 sequence;这些证据未完成前不能宣称 D7 或绝对零回归。 + +## 后续风险审查 + +- 不提前持久化半成品工具请求;工具 marker/parser 状态仍由现有 stream runtime 持有。 +- terminal/discard/dispose 会释放快照,迟到 chunk/terminal 继续受 operation identity 保护。 +- history load 与 terminal 同时发生的覆盖竞态已通过 ChatManager 回归测试并采用 active identity 保护;后续若引入更强的 revision 模型,仍应在 history owner 增加 revision guard,而不是让 transient provider 写盘。 diff --git a/docs/ui-interaction-accessibility-roadmap.md b/docs/ui-interaction-accessibility-roadmap.md index d8b7b03c0..7a829a901 100644 --- a/docs/ui-interaction-accessibility-roadmap.md +++ b/docs/ui-interaction-accessibility-roadmap.md @@ -1,9 +1,9 @@ # VCPChat UI 交互与可访问性行为合同路线 -> 状态:施工中 -> 基线:P1–P3 已完成,P4 仍需同步后的 Windows 复验与人工 soak -> 目的:把“可访问性、键盘、焦点、异步终态、视觉稳定性和资源清理”从分散的实现细节提升为可重复验证的 UI 行为合同。 -> 上位路线:[`next-ui-development-roadmap.md`](./next-ui-development-roadmap.md) +> 状态:规划中 +> 基线:P1–P3 已完成,P4 仍需同步后的 Windows 复验与人工 soak +> 目的:把“可访问性、键盘、焦点、异步终态、视觉稳定性和资源清理”从分散的实现细节提升为可重复验证的 UI 行为合同。 +> 上位路线:[`next-ui-development-roadmap.md`](./next-ui-development-roadmap.md) > 当前事实:[`next-ui-current-state.md`](./next-ui-current-state.md) ## 1. 为什么建立这条路线 @@ -45,29 +45,17 @@ open | 阶段 | 状态 | 目标 | 主要产物 | 退出条件 | |---|---|---|---|---| -| A0 事实与风险基线 | 已完成 | 盘点所有交互层、焦点入口、ARIA 和真实消费者 | `scripts/ui-interaction-inventory.json`、缺口报告、审计门禁 | 每个高频 Surface 有 owner、触发源和终态负责人,且清单引用真实入口 | -| A1 基础交互合同 | 核心完成;跨平台证据待验 | 统一 IconButton、focus-visible、ARIA 状态和 Escape 作用域 | 基础规则、静态 gate、键盘工具 | 主窗口关键导航和菜单通过键盘/ARIA 矩阵;Windows 复验仍待补 | -| A2 Modal/Popover/Select 内核 | 核心完成;打包与人工证据待验 | 消除误关闭、焦点丢失和层级竞争 | Overlay/Focus contract、真实测试 | 创建、设置、Ask Nova、Account Menu、托盘路径闭环;外部环境仍待验 | -| A3 异步终态与错误恢复 | 核心完成;长时 soak 待验 | 统一 loading、disabled、error、提交和取消行为 | 页面状态矩阵、错误字段合同 | 成功/失败/取消/迟到 Promise 均有唯一终态;稳定周期仍待验 | -| A4 主题、动画、DPI 与 fallback | macOS 自动完成;Windows/DPI/GPU 待验 | 让视觉内核和平台降级遵守同一交互合同 | 主题真源 gate、motion/DPI 矩阵 | macOS 自动证据通过;Windows 100/125/150% DPI、GPU 和视觉证据待补 | -| A5 页面模式和任务级回归 | 自动任务完成;人工视觉证据待验 | 把设置、创建、搜索、App、聊天变成固定工作流 | Task journey tests、截图基线 | 高频任务可从真实入口完成并验证世界状态;人工回归待补 | -| A6 交付门禁与稳定周期 | 自动门禁完成;外部发布证据待验 | 形成持续回归和发布证据 | `npm run check:ui-harness-evidence`、人工 soak 记录、P5 观测 | 门禁通过、签名打包启动和 30–60 分钟 soak 仍待补 | +| A0 事实与风险基线 | 未开始 | 盘点所有交互层、焦点入口、ARIA 和真实消费者 | Surface/键盘/属性清单、缺口报告 | 每个高频 Surface 有 owner、触发源和终态负责人 | +| A1 基础交互合同 | 未开始 | 统一 IconButton、focus-visible、ARIA 状态和 Escape 作用域 | 基础规则、静态 gate、键盘工具 | 主窗口关键导航和菜单通过键盘/ARIA 矩阵 | +| A2 Modal/Popover/Select 内核 | 未开始 | 消除误关闭、焦点丢失和层级竞争 | Overlay/Focus contract、真实测试 | 创建、设置、Ask Nova、Account Menu、托盘路径闭环 | +| A3 异步终态与错误恢复 | 未开始 | 统一 loading、disabled、error、提交和取消行为 | 页面状态矩阵、错误字段合同 | 成功/失败/取消/迟到 Promise 均有唯一终态 | +| A4 主题、动画、DPI 与 fallback | 未开始 | 让视觉内核和平台降级遵守同一交互合同 | 主题真源 gate、motion/DPI 矩阵 | Windows/macOS 关键主题和窗口尺寸通过视觉与键盘验证 | +| A5 页面模式和任务级回归 | 未开始 | 把设置、创建、搜索、App、聊天变成固定工作流 | Task journey tests、截图基线 | 高频任务可从真实入口完成并验证世界状态 | +| A6 交付门禁与稳定周期 | 未开始 | 形成持续回归和发布证据 | CI/本地矩阵、人工 soak 记录、P5 观测 | 一个稳定发布周期无阻塞性回归 | | A7 条件式逐页演进 | 条件式远期 | 仅在真实需求下迁移一个业务子页面 | 独立页面 PR | consumer/runtime/test/teardown 同步进入 | A0–A6 是完整落地这套行为合同所需的主路线;A7 不应提前进入当前主 PR。 -2026-08-19 补充:动态 AppTab 已改为独立 tab button、sibling close button 与 labelled tabpanel;全局设置分类已具备垂直 tablist、方向键/Home/End 和 section 关系;应用托盘设置弹窗已具备 dialog 语义、focus trap、Escape owner 与迟到 overlay acquire 清理。 - -设置 Electron gate 已实际通过保存、reload 恢复、分类/搜索、主题截图和 canonical Next 重载检查;旧的“双布局 teardown”断言已删除,避免将历史 Classic/Next 测试契约误当成当前主窗口架构。 - -同一真实 Electron smoke 现在额外连续执行 20 次明暗主题切换,并验证最终主题以及 Web Awesome theme link/owner 数量不漂移;这覆盖 Windows 上常见的高频切换候选竞态,但不替代 Windows 实机性能证据。 - -本机曾生成 macOS unpacked `.app` 并审计 `app.asar` 资源,但未签名产物直接启动没有建立调试端点,因此 packaged launch 仍标记为外部证据,不能用生成成功替代真实启动。 - -Windows、签名 packaged launch 与人工 soak 的执行步骤和归档字段见 [`ui-harness-external-evidence-checklist.md`](./ui-harness-external-evidence-checklist.md)。 - -2026-08-19 进展:交互 inventory 现在必须解析到真实存在的测试文件;任务 journey gate 进一步要求每条声明的 Electron 证据包含关键入口、操作和世界状态标记,避免“文件存在”被误报为覆盖。通知菜单具备独立 controller 证据,旧 document 级重复绑定已移除;Escape 使用单一优先级 owner;Launchpad 具备方向键/Home/End、打开时焦点转移与 fallback listener teardown。补充修复了动态 Launchpad 和应用托盘抽屉的隐藏可访问性:关闭时由各自 owner 设置 `inert`,避免运行时生成的应用按钮在隐藏或出场动画期间进入 Tab 顺序;对应 controller/Electron 回归覆盖打开/关闭终态。Ask Nova tabs 现在具备 roving tabindex、方向键导航和 tabpanel 关系;主聊天流式发送通过 `aria-busy` 暴露 loading 终态;聊天模式浮层同步 `aria-hidden`/`inert`,快速切换保存按最新意图串行化并避免旧失败回滚新选择。OverlayCoordinator dispose 现在会主动 reconcile 原生 WebContentsView,避免隐藏 View 遗留;AppTabHost 关闭当前动态 tab 时恢复焦点到相邻 tab 或 Home;native Modal 具备可访问名称;兼容确认框保留上游 API 但补齐同步 active、焦点陷阱、Escape 捕获与焦点恢复。macOS Electron UI Apps、主聊天序列、20 轮生命周期压力和 pack closure 均通过。A1–A6 仍保持进行中,直至异步状态逐项真实终态、Windows/打包产物、人工 soak 与稳定周期证据完成。 - ## 4. A0:事实与风险基线 ### 4.1 盘点范围 diff --git a/docs/ui-system.md b/docs/ui-system.md index ede7f1b8a..dda2ff6db 100644 --- a/docs/ui-system.md +++ b/docs/ui-system.md @@ -148,4 +148,4 @@ npm run check:ui-system 真实 Electron 验证记录与剩余矩阵见 [新版 UI Electron QA 矩阵](./ui-system-qa-matrix.md)。 -应用页与全局设置迁移见 [VCPChat 应用页面与全局设置 Web Awesome 全量重构计划](./ui-applications-webawesome-migration-plan.md)。 +应用页整页 Web Awesome 迁移已经停止;历史方案见 [归档路线](./archive/2026-08-chat-kernel-and-ui-roadmaps/ui-applications-webawesome-migration-plan.md)。未来只按真实业务需求在独立 PR 中同时引入页面、runtime、consumer 与测试。 diff --git a/main.html b/main.html index 9cfa83f36..b3521e387 100755 --- a/main.html +++ b/main.html @@ -2503,11 +2503,9 @@

添加默认允许工具

- - @@ -2524,7 +2522,9 @@

添加默认允许工具

- + + + @@ -2552,7 +2552,6 @@

添加默认允许工具

- diff --git a/modules/chat/chatContext.js b/modules/chat/chatContext.js new file mode 100644 index 000000000..a91a60d0b --- /dev/null +++ b/modules/chat/chatContext.js @@ -0,0 +1,30 @@ +/** + * Explicit chat-surface context. It is deliberately small: this is a state + * boundary, not a replacement Store. Domain-facing code can observe/update + * the active item, topic and history without knowing the renderer's globals. + */ +export function createChatContext(initial = {}) { + let selectedItem = initial.selectedItem ?? { id: null, type: null, name: null, avatarUrl: null, config: null }; + let topicId = initial.topicId ?? null; + let history = Array.isArray(initial.history) ? [...initial.history] : []; + const listeners = new Set(); + + const emit = (kind, value) => { + for (const listener of [...listeners]) { + try { listener({ kind, value, context }); } catch (error) { + console.warn('[ChatContext] listener failed:', error); + } + } + }; + const context = { + get selectedItem() { return selectedItem; }, + get topicId() { return topicId; }, + get history() { return [...history]; }, + setSelectedItem(value) { selectedItem = value ?? { id: null, type: null, name: null, avatarUrl: null, config: null }; emit('selected-item', selectedItem); return selectedItem; }, + setTopicId(value) { topicId = value ?? null; emit('topic', topicId); return topicId; }, + setHistory(value) { history = Array.isArray(value) ? [...value] : []; emit('history', [...history]); return [...history]; }, + subscribe(listener) { if (typeof listener !== 'function') throw new TypeError('ChatContext listener must be a function'); listeners.add(listener); return () => listeners.delete(listener); }, + dispose() { listeners.clear(); } + }; + return context; +} diff --git a/modules/chat/chatDomRenderer.js b/modules/chat/chatDomRenderer.js new file mode 100644 index 000000000..8b0868be3 --- /dev/null +++ b/modules/chat/chatDomRenderer.js @@ -0,0 +1,50 @@ +/** + * DOM adapter for a chat surface. It owns the root contract and delegates + * rendering operations to the existing renderer during the migration. + */ +export function createChatDomRenderer({ root, renderer, disposeRenderer = null }) { + if (!root || typeof root.querySelector !== 'function') throw new TypeError('ChatDomRenderer requires a root element'); + if (!renderer) throw new TypeError('ChatDomRenderer requires a renderer adapter'); + let disposed = false; + const pending = new Set(); + const ownedDisposers = []; + const assertActive = () => { if (disposed) throw new Error('ChatDomRenderer is disposed'); }; + const track = (operation) => { + const promise = Promise.resolve(operation); + pending.add(promise); + promise.finally(() => pending.delete(promise)).catch(() => {}); + return promise; + }; + return { + get root() { return root; }, + own(disposer) { assertActive(); if (typeof disposer !== 'function') throw new TypeError('ChatDomRenderer owner requires a disposer'); ownedDisposers.push(disposer); }, + renderMessage(...args) { if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); const options = args[4] || {}; args[4] = { ...options, root }; return track(renderer.renderMessage(...args)); }, + renderBatch(...args) { if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); const options = args[3] || {}; args[3] = { ...options, root }; return track(renderer.renderMessageBatch(...args)); }, + renderHistory(...args) { if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); const options = args[1] || {}; args[1] = { ...options, root }; return track(renderer.renderHistory(...args)); }, + updateStreaming(...args) { if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); return track(renderer.updateMessageContent?.(...args, root)); }, + startStreaming(...args) { + if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); + return track(renderer.startStreamingMessage?.(...args)); + }, + appendStreaming(...args) { + if (disposed) return false; + renderer.appendStreamChunk?.(...args); + return true; + }, + projectStreamTerminal(...args) { + if (disposed) return Promise.reject(new Error('ChatDomRenderer is disposed')); + return track(renderer.projectStreamTerminal?.(...args)); + }, + discardStreaming(...args) { if (disposed) return; return renderer.discardStreamingMessage?.(...args); }, + removeMessage(...args) { assertActive(); return track(renderer.removeMessageById(...args, root)); }, + clear() { assertActive(); return track(renderer.clearChat?.(root)); }, + async dispose({ clear = false } = {}) { + if (disposed) return; + disposed = true; + await Promise.allSettled(ownedDisposers.splice(0).reverse().map(disposeOwned => Promise.resolve().then(disposeOwned))); + await Promise.allSettled([...pending]); + await disposeRenderer?.(); + if (clear) root.replaceChildren(); + } + }; +} diff --git a/modules/chat/chatEventContract.js b/modules/chat/chatEventContract.js new file mode 100644 index 000000000..5d3e8f239 --- /dev/null +++ b/modules/chat/chatEventContract.js @@ -0,0 +1,42 @@ +/** + * Typed vocabulary shared by stream producers and Surface consumers. + * This module is deliberately DOM/Electron-free so source-plane tests can + * validate terminal ownership without booting a renderer. + */ +export const STREAM_EVENT_MODE = 'emit'; +export const STREAM_TERMINAL_KINDS = Object.freeze(['completed', 'failed', 'cancelled', 'discarded']); +const TERMINAL_SET = new Set(STREAM_TERMINAL_KINDS); + +export const CHAT_EVENT_CONTRACTS = Object.freeze({ + 'chat.stream.started': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: false }), + 'chat.stream.chunk': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: false }), + 'chat.stream.final': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: false }), + 'chat.stream.completed': Object.freeze({ durable: true, mode: STREAM_EVENT_MODE, terminal: true }), + 'chat.stream.failed': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: true }), + 'chat.stream.cancelled': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: true }), + 'chat.stream.discarded': Object.freeze({ durable: false, mode: STREAM_EVENT_MODE, terminal: true }), +}); + +export function normalizeChatTerminal(input, fallback = {}) { + const source = typeof input === 'string' ? { kind: input } : (input && typeof input === 'object' ? input : {}); + const raw = String(source.kind || source.type || source.finishReason || '').toLowerCase(); + const kind = ['end', 'done', 'success', 'stop', 'completed'].includes(raw) + ? 'completed' + : ['abort', 'aborted', 'cancel', 'cancelled'].includes(raw) + ? 'cancelled' + : ['discard', 'discarded', 'stale'].includes(raw) + ? 'discarded' + : ['error', 'failed', 'disconnect', 'disconnected', 'reader-error'].includes(raw) + ? 'failed' + : fallback.kind; + if (!TERMINAL_SET.has(kind)) throw new TypeError(`unknown chat stream terminal: ${raw || ''}`); + return Object.freeze({ ...fallback, ...source, kind }); +} + +export function assertChatEventContract({ name, terminal = false, durable = false } = {}) { + const contract = CHAT_EVENT_CONTRACTS[name]; + if (!contract) throw new Error(`unregistered chat event: ${name}`); + if (Boolean(terminal) !== contract.terminal) throw new Error(`terminal declaration mismatch for ${name}`); + if (Boolean(durable) !== contract.durable) throw new Error(`durability declaration mismatch for ${name}`); + return contract; +} diff --git a/modules/chat/chatHistoryMutationAuthority.js b/modules/chat/chatHistoryMutationAuthority.js new file mode 100644 index 000000000..d215e1507 --- /dev/null +++ b/modules/chat/chatHistoryMutationAuthority.js @@ -0,0 +1,77 @@ +const conversationKey = ({ itemId, itemType = 'agent', topicId }) => { + if (!itemId || !topicId) throw new TypeError('History mutation requires itemId and topicId'); + return `${itemType}:${itemId}/topic:${topicId}`; +}; + +/** + * Single durable write authority for non-stream chat-history mutations. + * Callers still own domain intent; this provider owns serialization, repository + * commit and the one observable operation Promise for each mutation. + */ +export function createChatHistoryMutationAuthority({ repository } = {}) { + if (!repository || typeof repository.getHistory !== 'function' || typeof repository.saveHistory !== 'function') { + throw new TypeError('ChatHistoryMutationAuthority requires a ChatRepository'); + } + const queues = new Map(); + let disposed = false; + + const run = (descriptor, operation) => { + if (disposed) return Promise.reject(new Error('ChatHistoryMutationAuthority is disposed')); + const key = conversationKey(descriptor); + const previous = queues.get(key) || Promise.resolve(); + const current = previous.catch(() => {}).then(operation); + queues.set(key, current); + current.finally(() => { + if (queues.get(key) === current) queues.delete(key); + }).catch(() => {}); + return current; + }; + + const commit = async (descriptor, history) => { + if (!Array.isArray(history)) throw new TypeError('History mutation commit requires an array'); + const snapshot = structuredClone(history); + const result = await repository.saveHistory( + descriptor.itemId, + descriptor.itemType || 'agent', + descriptor.topicId, + snapshot + ); + if (result?.success === false) throw new Error(result.error || 'History mutation commit failed'); + return Object.freeze({ + category: descriptor.category || 'replace', + itemId: descriptor.itemId, + itemType: descriptor.itemType || 'agent', + topicId: descriptor.topicId, + history: Object.freeze(snapshot), + result, + }); + }; + + return Object.freeze({ + replace(descriptor, history) { + return run(descriptor, () => commit(descriptor, history)); + }, + mutate(descriptor, transform) { + if (typeof transform !== 'function') throw new TypeError('History mutation requires a transform'); + return run(descriptor, async () => { + const current = await repository.getHistory( + descriptor.itemId, + descriptor.itemType || 'agent', + descriptor.topicId + ); + if (!Array.isArray(current)) throw new Error(current?.error || 'History mutation read failed'); + const next = await transform(structuredClone(current)); + return commit(descriptor, next); + }); + }, + diagnostics() { + return Object.freeze({ disposed, pendingConversations: queues.size }); + }, + async dispose() { + if (disposed) return; + disposed = true; + await Promise.allSettled([...queues.values()]); + queues.clear(); + }, + }); +} diff --git a/modules/chat/chatHistoryPersistence.js b/modules/chat/chatHistoryPersistence.js new file mode 100644 index 000000000..9f786e089 --- /dev/null +++ b/modules/chat/chatHistoryPersistence.js @@ -0,0 +1,81 @@ +const messageIdentity = message => ( + message?.id || `${message?.role || ''}:${message?.timestamp || ''}:${message?.content || ''}` +); + +const cloneHistory = history => history.map(message => ({ ...message })); + +/** + * Creates the durable terminal-history provider used by stream coordinators. + * Serialization belongs to the coordinator; this provider owns repository + * read/merge/write policy and propagates every durable failure to its caller. + */ +function createHistoryPersistence(repository, { durable, skipTemporaryContexts }) { + if (!repository || typeof repository.getHistory !== 'function' || typeof repository.saveHistory !== 'function') { + throw new TypeError('ChatHistoryPersistence requires a chat repository'); + } + + return Object.freeze({ + async commit(projected) { + if (!projected?.context || !Array.isArray(projected.history)) return projected || null; + + const context = projected.context; + if (skipTemporaryContexts && (context.topicId === 'assistant_chat' || context.isGroupMessage)) { + return Object.freeze({ + messageId: projected.messageId, + context, + content: projected.content, + finishReason: projected.finishReason, + }); + } + + const itemId = context.groupId || context.agentId; + const itemType = context.isGroupMessage ? 'group' : 'agent'; + if (!itemId || !context.topicId) { + throw new Error('Stream terminal persistence requires item and topic identity'); + } + + const projectedHistory = cloneHistory( + projected.history.filter(message => !message?.isThinking && !message?.isPendingStream) + ); + const durableHistory = await repository.getHistory(itemId, itemType, context.topicId); + if (!Array.isArray(durableHistory)) { + throw new Error('Chat repository returned invalid history during stream commit'); + } + + const projectedIds = new Set(projectedHistory.map(messageIdentity)); + const durableOnly = durableHistory + .filter(message => !projectedIds.has(messageIdentity(message))) + .map(message => ({ ...message })); + const mergedHistory = [...projectedHistory, ...durableOnly] + .sort((left, right) => { + const leftTime = Number(left?.timestamp); + const rightTime = Number(right?.timestamp); + if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime) || leftTime === rightTime) return 0; + return leftTime - rightTime; + }); + + await repository.saveHistory(itemId, itemType, context.topicId, mergedHistory); + return Object.freeze({ + messageId: projected.messageId, + context, + content: projected.content, + finishReason: projected.finishReason, + }); + }, + durable, + }); +} + +export function createChatHistoryPersistence(repository) { + return createHistoryPersistence(repository, { durable: true, skipTemporaryContexts: true }); +} + +/** + * Creates terminal persistence for a short-lived auxiliary session. + * The operation Promise settles only after the in-memory session snapshot is + * updated; the session owner separately commits that snapshot to durable + * history when it creates the final topic during close. + */ +export function createTransientChatHistoryPersistence(repository) { + return createHistoryPersistence(repository, { durable: false, skipTemporaryContexts: false }); +} diff --git a/modules/chat/chatOperation.js b/modules/chat/chatOperation.js new file mode 100644 index 000000000..17851fe19 --- /dev/null +++ b/modules/chat/chatOperation.js @@ -0,0 +1,34 @@ +/** A narrow operation contract for interactive chat surfaces. */ +export function createChatOperations({ send, cancel = null }) { + if (typeof send !== 'function') throw new TypeError('ChatOperations requires send()'); + let active = null; + let cancelRequested = false; + let disposed = false; + return { + get state() { return Object.freeze({ status: disposed ? 'disposed' : active ? 'pending' : 'idle' }); }, + async sendMessage(request) { + if (disposed) throw new Error('ChatOperations is disposed'); + if (active) throw new Error('A chat operation is already pending'); + cancelRequested = false; + let operation; + try { operation = Promise.resolve(send(request)); } + catch (error) { operation = Promise.reject(error); } + active = operation; + try { return await operation; } + finally { if (active === operation) active = null; } + }, + async cancel() { + if (!active || typeof cancel !== 'function') return false; + if (cancelRequested) return true; + cancelRequested = true; + await cancel(); + return true; + }, + async dispose() { + if (disposed) return; + disposed = true; + if (active && typeof cancel === 'function') await cancel(); + if (active) await Promise.allSettled([active]); + } + }; +} diff --git a/modules/chat/chatPluginManifest.js b/modules/chat/chatPluginManifest.js new file mode 100644 index 000000000..add125c39 --- /dev/null +++ b/modules/chat/chatPluginManifest.js @@ -0,0 +1,81 @@ +const CAPABILITIES = new Set(['theme', 'surface-slot', 'presentation-state']); +const SLOTS = new Set(['header', 'message-tail']); + +export function validateChatPluginManifest(manifest) { + if (!manifest || typeof manifest !== 'object') throw new TypeError('chat plugin manifest is required'); + if (!manifest.id || !/^[a-z0-9-]+$/.test(manifest.id)) throw new Error('chat plugin id must be lowercase kebab-case'); + if (manifest.apiVersion !== 1) throw new Error('unsupported chat plugin API version'); + const capabilities = Array.isArray(manifest.capabilities) ? manifest.capabilities : []; + for (const capability of capabilities) if (!CAPABILITIES.has(capability)) throw new Error(`unsupported chat plugin capability: ${capability}`); + for (const slot of manifest.slots || []) if (!SLOTS.has(slot)) throw new Error(`unsupported chat plugin slot: ${slot}`); + if (manifest.ipc || manifest.selectors || manifest.cssText) throw new Error('chat plugins cannot request IPC, selectors, or CSS text'); + return Object.freeze({ id: manifest.id, apiVersion: 1, capabilities: Object.freeze([...capabilities]), slots: Object.freeze([...(manifest.slots || [])]) }); +} + +export function createChatPluginLoader({ state, slots, theme, skins = new Map() }) { + const installed = new Map(); + let disposed = false; + return { + install(rawManifest, provider) { + if (disposed) throw new Error('chat plugin loader is disposed'); + const manifest = validateChatPluginManifest(rawManifest); + if (installed.has(manifest.id)) throw new Error(`chat plugin already installed: ${manifest.id}`); + if (typeof provider !== 'function') throw new TypeError('chat plugin provider is required'); + const owned = []; + const releaseOwned = () => { + [...owned].reverse().forEach(fn => { try { fn(); } catch (error) { console.error('[ChatPluginLoader] owner teardown failed:', error); } }); + owned.length = 0; + }; + const subscribeState = (listener) => { + if (!manifest.capabilities.includes('presentation-state')) throw new Error('presentation-state capability not declared'); + if (typeof listener !== 'function' || typeof state?.subscribe !== 'function') throw new TypeError('presentation state subscription is required'); + const unsubscribe = state.subscribe(snapshot => listener(Object.freeze({ ...snapshot }))); + if (typeof unsubscribe === 'function') owned.push(unsubscribe); + return unsubscribe; + }; + let dispose; + try { + dispose = provider(Object.freeze({ + manifest, + state: state?.getSnapshot?.() || Object.freeze({}), + subscribeState, + registerSlot: (slot, id, mount) => { + if (!manifest.slots.includes(slot)) throw new Error(`slot not declared by plugin: ${slot}`); + const disposeSlot = slots?.register(slot, `${manifest.id}:${id}`, mount); + if (typeof disposeSlot === 'function') owned.push(disposeSlot); + return disposeSlot; + }, + applyTheme: (root, plugin) => { + if (!manifest.capabilities.includes('theme')) throw new Error('theme capability not declared'); + const apply = plugin?.apply || theme?.apply; + if (typeof apply !== 'function') throw new TypeError('theme plugin is required'); + const teardown = apply(root); + if (typeof teardown === 'function') owned.push(teardown); + return teardown; + }, + mountSkin: (root, skin) => { + if (!manifest.capabilities.includes('presentation-state')) throw new Error('presentation-state capability not declared'); + const mount = skin?.mount || skins.get(manifest.id)?.mount; + if (typeof mount !== 'function') throw new TypeError('presentation skin is required'); + const mounted = mount(root, state?.getSnapshot?.() || Object.freeze({})); + const teardown = typeof mounted === 'function' ? mounted : mounted?.teardown; + const update = typeof mounted === 'function' ? mounted.update : mounted?.update; + if (typeof teardown === 'function') owned.push(teardown); + if (update && typeof state?.subscribe === 'function') { + const unsubscribe = state.subscribe(snapshot => update(snapshot)); + if (typeof unsubscribe === 'function') owned.push(unsubscribe); + } + return teardown; + } + })); + } catch (error) { + releaseOwned(); + throw error; + } + const teardown = typeof dispose === 'function' ? dispose : () => {}; + installed.set(manifest.id, () => { releaseOwned(); teardown(); }); + return () => { if (!installed.has(manifest.id)) return; installed.delete(manifest.id); releaseOwned(); teardown(); }; + }, + dispose() { disposed = true; [...installed.values()].reverse().forEach(teardown => teardown()); installed.clear(); } + }; +} diff --git a/modules/chat/chatPresentationSkin.js b/modules/chat/chatPresentationSkin.js new file mode 100644 index 000000000..7d3b97abf --- /dev/null +++ b/modules/chat/chatPresentationSkin.js @@ -0,0 +1,19 @@ +/** Controlled presentation skin: token-only, state-read-only, owned teardown. */ +export function createPresentationSkin({ id, tokens = {}, render, update = null }) { + if (!id || !/^[a-z0-9-]+$/.test(id)) throw new TypeError('skin id must be lowercase kebab-case'); + if (typeof render !== 'function') throw new TypeError('skin render function is required'); + if (update !== null && typeof update !== 'function') throw new TypeError('skin update must be a function'); + const frozenTokens = Object.freeze({ ...tokens }); + return Object.freeze({ + id, + tokens: frozenTokens, + mount(root, state) { + if (!root) throw new TypeError('skin root is required'); + const snapshot = Object.freeze({ ...state }); + const renderedTeardown = render(root, snapshot, frozenTokens); + const teardown = typeof renderedTeardown === 'function' ? renderedTeardown : () => {}; + if (update) teardown.update = (nextState) => update(root, Object.freeze({ ...nextState }), frozenTokens); + return teardown; + } + }); +} diff --git a/modules/chat/chatPresentationState.js b/modules/chat/chatPresentationState.js new file mode 100644 index 000000000..e7b47e24e --- /dev/null +++ b/modules/chat/chatPresentationState.js @@ -0,0 +1,33 @@ +/** Read-only presentation state channel for skins and named slots. */ +export function createChatPresentationState(initial = {}) { + let state = { mode: 'idle', theme: initial.theme || 'light', reducedMotion: Boolean(initial.reducedMotion), activeSurface: initial.activeSurface || 'main' }; + let disposed = false; + const listeners = new Set(); + const snapshot = () => Object.freeze({ ...state }); + return { + getSnapshot: snapshot, + set(patch = {}) { + if (disposed) return snapshot(); + state = { ...state, ...patch }; + const next = snapshot(); + for (const listener of [...listeners]) { + try { listener(next); } catch (error) { + // A presentation consumer cannot be allowed to break the + // authoritative state transition or other subscribers. + console.error('[ChatPresentationState] subscriber failed:', error); + } + } + return next; + }, + subscribe(listener) { + if (disposed) return () => {}; + if (typeof listener !== 'function') throw new TypeError('presentation state listener is required'); + listeners.add(listener); + try { listener(snapshot()); } catch (error) { + console.error('[ChatPresentationState] subscriber failed during initial snapshot:', error); + } + return () => listeners.delete(listener); + }, + dispose() { disposed = true; listeners.clear(); } + }; +} diff --git a/modules/chat/chatRepository.js b/modules/chat/chatRepository.js new file mode 100644 index 000000000..1729dd8c4 --- /dev/null +++ b/modules/chat/chatRepository.js @@ -0,0 +1,24 @@ +/** + * Electron-free contract for chat history persistence. The adapter is the + * only place where agent/group IPC naming is selected; callers depend on the + * domain-shaped get/save methods instead of the transport API. + */ +export function createChatRepository(electronAPI) { + if (!electronAPI) throw new TypeError('ChatRepository requires an IPC adapter'); + const method = (name) => { + if (typeof electronAPI[name] !== 'function') throw new Error(`Missing chat repository capability: ${name}`); + return electronAPI[name].bind(electronAPI); + }; + return Object.freeze({ + getHistory(itemId, itemType, topicId) { + return itemType === 'group' + ? method('getGroupChatHistory')(itemId, topicId) + : method('getChatHistory')(itemId, topicId); + }, + saveHistory(itemId, itemType, topicId, history) { + return itemType === 'group' + ? method('saveGroupChatHistory')(itemId, topicId, history) + : method('saveChatHistory')(itemId, topicId, history); + } + }); +} diff --git a/modules/chat/chatSurface.js b/modules/chat/chatSurface.js new file mode 100644 index 000000000..91d278c25 --- /dev/null +++ b/modules/chat/chatSurface.js @@ -0,0 +1,59 @@ +import { createChatDomRenderer } from './chatDomRenderer.js'; + +/** + * Lifecycle owner for a chat presentation surface. The first consumer is the + * main chat; the same contract can later host a standalone read-only view. + */ +export function createChatSurface({ root, renderer, repository, focusTarget = null, mode = 'readonly', slots = null, operations = null, presentationState = null, disposeRenderer = null, conversation = null }) { + if (!root) throw new TypeError('ChatSurface requires a root element'); + const domRenderer = createChatDomRenderer({ root, renderer, disposeRenderer }); + let disposed = false; + let operationToken = 0; + const mountedSlotDisposers = []; + root.dataset.chatSurface = mode; + root.setAttribute('aria-live', 'polite'); + + const surface = { + root, + renderer: domRenderer, + operations, + get disposed() { return disposed; }, + async loadHistory(itemId, itemType, topicId, options) { + if (disposed) throw new Error('ChatSurface is disposed'); + const token = ++operationToken; + const history = await repository.getHistory(itemId, itemType, topicId); + if (disposed || token !== operationToken) return { stale: true }; + conversation?.replaceHistory?.(history); + await domRenderer.renderHistory(history, options); + return { history, stale: false }; + }, + focus() { if (!disposed) (focusTarget || root).focus?.(); }, + mountSlot(slot, slotRoot, snapshot = {}) { + if (disposed || !slots) return; + mountedSlotDisposers.push(...slots.mount(slot, slotRoot, { mode, ...(presentationState?.getSnapshot?.() || {}), ...snapshot })); + }, + sendMessage(request) { + if (!operations) return Promise.reject(new Error('ChatSurface has no send capability')); + return operations.sendMessage(request); + }, + cancelMessage() { return operations?.cancel?.() || Promise.resolve(false); }, + async dispose() { + if (disposed) return; + disposed = true; + operationToken += 1; + // The operation owns its terminal/cancellation promise. Drain it + // before retracting the renderer route so terminal persistence and + // projection cannot race an already-destroyed Surface root. + await operations?.dispose?.(); + await domRenderer.dispose(); + mountedSlotDisposers.splice(0).reverse().forEach(disposeSlot => disposeSlot()); + delete root.dataset.chatSurface; + root.removeAttribute('aria-live'); + } + }; + return surface; +} + +export function createReadOnlyChatSurface(options) { + return createChatSurface({ ...options, mode: 'readonly' }); +} diff --git a/modules/chat/chatSurfaceSlots.js b/modules/chat/chatSurfaceSlots.js new file mode 100644 index 000000000..46c4f8c9a --- /dev/null +++ b/modules/chat/chatSurfaceSlots.js @@ -0,0 +1,42 @@ +/** + * Narrow named-slot contract for presentation extensions. A slot receives a + * read-only snapshot and an owned root; it cannot access business IPC or + * arbitrary selectors. Registration returns a disposer and late owners fail. + */ +export function createChatSurfaceSlots() { + const definitions = new Map(); + let disposed = false; + const allowed = new Set(['header', 'message-tail']); + return { + register(slot, id, mount) { + if (disposed) throw new Error('ChatSurfaceSlots is disposed'); + if (!allowed.has(slot)) throw new Error(`Unsupported chat surface slot: ${slot}`); + if (!id || typeof mount !== 'function') throw new TypeError('slot id and mount function are required'); + const key = `${slot}:${id}`; + if (definitions.has(key)) throw new Error(`Duplicate chat surface contribution: ${key}`); + definitions.set(key, { slot, id, mount }); + let active = true; + return () => { if (!active) return; active = false; definitions.delete(key); }; + }, + mount(slot, root, snapshot) { + if (disposed) return []; + const owned = []; + for (const definition of definitions.values()) { + if (definition.slot !== slot) continue; + const child = root.ownerDocument.createElement('span'); + child.dataset.chatSlotOwner = definition.id; + root.appendChild(child); + try { + const unmount = definition.mount(child, Object.freeze({ ...snapshot })); + owned.push(() => { if (typeof unmount === 'function') unmount(); child.remove(); }); + } catch (error) { + child.remove(); + owned.splice(0).reverse().forEach(dispose => dispose()); + throw error; + } + } + return owned; + }, + dispose() { disposed = true; definitions.clear(); } + }; +} diff --git a/modules/chat/chatThemePlugin.js b/modules/chat/chatThemePlugin.js new file mode 100644 index 000000000..f51ef57a9 --- /dev/null +++ b/modules/chat/chatThemePlugin.js @@ -0,0 +1,14 @@ +const ALLOWED_TOKENS = new Set(['accent', 'surface', 'text', 'muted', 'bubble']); + +/** Token-only theme extension. Arbitrary CSS text/selectors are rejected. */ +export function createChatThemePlugin({ id, tokens = {} }) { + if (!id || !/^[a-z0-9-]+$/.test(id)) throw new TypeError('theme id must be lowercase kebab-case'); + const invalid = Object.keys(tokens).filter(token => !ALLOWED_TOKENS.has(token)); + if (invalid.length) throw new Error(`Unsupported chat theme token: ${invalid.join(', ')}`); + const frozen = Object.freeze({ ...tokens }); + return Object.freeze({ id, tokens: frozen, apply(root) { + if (!root) throw new TypeError('theme root is required'); + for (const [name, value] of Object.entries(frozen)) root.style.setProperty(`--chat-${name}`, String(value)); + return () => Object.keys(frozen).forEach(name => root.style.removeProperty(`--chat-${name}`)); + } }); +} diff --git a/modules/chat/contentModes.js b/modules/chat/contentModes.js new file mode 100644 index 000000000..73e17dc2d --- /dev/null +++ b/modules/chat/contentModes.js @@ -0,0 +1,5 @@ +/** Processing modes shared by the DOM-free content runtime and its adapters. */ +export const PIPELINE_MODES = Object.freeze({ + FULL_RENDER: 'full-render', + STREAM_FAST: 'stream-fast' +}); diff --git a/modules/chat/contentRuntime.js b/modules/chat/contentRuntime.js new file mode 100644 index 000000000..1107d96bb --- /dev/null +++ b/modules/chat/contentRuntime.js @@ -0,0 +1,75 @@ +import { PIPELINE_MODES } from './contentModes.js'; + +export function normalizeStreamChunk(chunk) { + if (chunk?.error === 'json_parse_error') return ''; + if (typeof chunk?.choices?.[0]?.delta?.content === 'string') return chunk.choices[0].delta.content; + if (typeof chunk?.delta?.content === 'string') return chunk.delta.content; + if (typeof chunk?.content === 'string') return chunk.content; + if (typeof chunk === 'string') return chunk; + if (typeof chunk?.raw === 'string' && !chunk?.error) return chunk.raw; + return ''; +} + +/** + * Content Runtime contract. It owns content normalization/preprocessing but + * deliberately knows nothing about DOM, Electron or a chat surface. + */ +export function createContentRuntime({ pipeline }) { + if (!pipeline || typeof pipeline.process !== 'function') { + throw new TypeError('ContentRuntime requires a content pipeline'); + } + const process = (text, mode, options = {}) => pipeline.process(text, { ...options, mode }); + const normalizeAttachments = (attachments) => { + if (!Array.isArray(attachments)) return Object.freeze([]); + return Object.freeze(attachments.filter(Boolean).map((attachment) => Object.freeze({ + ...attachment, + type: typeof attachment.type === 'string' ? attachment.type : 'application/octet-stream', + name: typeof attachment.name === 'string' && attachment.name.trim() + ? attachment.name + : '未命名附件', + src: typeof attachment.src === 'string' ? attachment.src : '' + }))); + }; + return Object.freeze({ + normalizeAttachments, + normalizeMessage(message, options) { + const source = message && typeof message === 'object' ? message : {}; + return { + ...source, + id: source.id || options?.fallbackId || `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + role: source.role || 'assistant', + content: source.content === undefined ? '' : source.content, + attachments: normalizeAttachments(source.attachments) + }; + }, + processFull(text, options = {}) { + return process(text, PIPELINE_MODES.FULL_RENDER, options); + }, + createRenderModel(message, options = {}) { + const normalized = this.normalizeMessage(message, options); + const result = options.mode === PIPELINE_MODES.STREAM_FAST + ? this.processStream(normalized.content, options) + : this.processFull(normalized.content, options); + return Object.freeze({ + message: Object.freeze(normalized), + text: result.text, + state: result.state, + mode: options.mode || PIPELINE_MODES.FULL_RENDER + }); + }, + processStream(text, options = {}) { + return process(text, PIPELINE_MODES.STREAM_FAST, options); + }, + extractChunkText(chunk) { + return normalizeStreamChunk(chunk); + }, + createStreamAssembler(initial = '') { + let text = typeof initial === 'string' ? initial : ''; + return Object.freeze({ + append(chunk) { text += typeof chunk === 'string' ? chunk : ''; return text; }, + get text() { return text; }, + reset(value = '') { text = typeof value === 'string' ? value : ''; return text; } + }); + } + }); +} diff --git a/modules/chat/contentTransforms.js b/modules/chat/contentTransforms.js new file mode 100644 index 000000000..2448cae80 --- /dev/null +++ b/modules/chat/contentTransforms.js @@ -0,0 +1,20 @@ +/** Pure content transforms used by ContentRuntime; no DOM or Electron access. */ +const HTML_ENTITIES = Object.freeze({ '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'" }); + +export function decodeHtmlEntities(value) { + return String(value ?? '').replace(/&(?:amp|lt|gt|quot|#39|apos);|&#x[0-9a-f]+;|&#\d+;/gi, token => { + if (HTML_ENTITIES[token]) return HTML_ENTITIES[token]; + const code = token.toLowerCase().startsWith('&#x') + ? Number.parseInt(token.slice(3, -1), 16) + : Number.parseInt(token.slice(2, -1), 10); + return Number.isFinite(code) ? String.fromCodePoint(code) : token; + }); +} + +export function createMermaidPlaceholderTransform({ escapeHtml = value => String(value ?? '') } = {}) { + const mermaidCodeRegex = /\s*(flowchart|graph|mermaid)\s+([\s\S]*?)<\/code>/gi; + const mermaidFenceRegex = /```(mermaid|flowchart|graph)[^\S\n]*\n([\s\S]*?)```/gi; + return text => String(text ?? '') + .replace(mermaidCodeRegex, (_match, _lang, code) => `
`) + .replace(mermaidFenceRegex, (_match, _lang, code) => `
`); +} diff --git a/modules/chat/mainChatStateAuthority.js b/modules/chat/mainChatStateAuthority.js new file mode 100644 index 000000000..7cab0461d --- /dev/null +++ b/modules/chat/mainChatStateAuthority.js @@ -0,0 +1,61 @@ +const freezeSelection = value => Object.freeze(value && typeof value === 'object' ? { ...value } : { + id: null, type: null, name: null, avatarUrl: null, config: null, +}); + +const cloneReadOnly = value => { + if (!value || typeof value !== 'object') return value; + if (Array.isArray(value)) return Object.freeze(value.map(cloneReadOnly)); + return Object.freeze(Object.fromEntries( + Object.entries(value).map(([key, nested]) => [key, cloneReadOnly(nested)]) + )); +}; + +/** Owns the main chat's current selection without exposing mutable globals. */ +export function createMainChatStateAuthority(initial = {}) { + let selectedItem = freezeSelection(initial.selectedItem); + let topicId = initial.topicId ?? null; + let history = Array.isArray(initial.history) ? [...initial.history] : []; + + const snapshot = () => Object.freeze({ selectedItem, topicId, history: Object.freeze([...history]) }); + const consumerSnapshot = () => Object.freeze({ + selectedItem: cloneReadOnly(selectedItem), + topicId, + history: cloneReadOnly(history), + }); + const historyRef = Object.freeze({ + get: () => history, + set(value) { + history = Array.isArray(value) ? [...value] : []; + return history; + }, + }); + const selectedItemRef = Object.freeze({ + get: () => selectedItem, + set(value) { + selectedItem = freezeSelection(value); + return selectedItem; + }, + }); + const topicIdRef = Object.freeze({ + get: () => topicId, + set(value) { + topicId = value ?? null; + return topicId; + }, + }); + const consumer = Object.freeze({ snapshot: consumerSnapshot }); + + return Object.freeze({ + consumer, + snapshot, + selectedItemRef, + topicIdRef, + setSelectedItem(value) { + return selectedItemRef.set(value); + }, + setTopicId(value) { + return topicIdRef.set(value); + }, + historyRef, + }); +} diff --git a/modules/chat/memoryChatRepository.js b/modules/chat/memoryChatRepository.js new file mode 100644 index 000000000..6b634b57f --- /dev/null +++ b/modules/chat/memoryChatRepository.js @@ -0,0 +1,18 @@ +const cloneHistory = history => Array.isArray(history) ? history.map(message => ({ ...message })) : []; + +/** Explicit short-lived repository for auxiliary chat windows with no durable topic. */ +export function createMemoryChatRepository({ read, write } = {}) { + if (typeof read !== 'function' || typeof write !== 'function') { + throw new TypeError('memory chat repository requires read and write capabilities'); + } + return Object.freeze({ + async getHistory() { + return cloneHistory(read()); + }, + async saveHistory(_itemId, _itemType, _topicId, history) { + const next = cloneHistory(history); + write(next); + return next; + }, + }); +} diff --git a/modules/chat/streamConsumerRegistry.js b/modules/chat/streamConsumerRegistry.js new file mode 100644 index 000000000..a5d07336b --- /dev/null +++ b/modules/chat/streamConsumerRegistry.js @@ -0,0 +1,55 @@ +/** Owner-scoped routing metadata; this is not a global event bus or stream state store. */ +export function createStreamConsumerRegistry() { + const routes = new Map(); + let disposed = false; + return Object.freeze({ + register(messageId, route) { + if (disposed) throw new Error('stream consumer registry is disposed'); + if (!messageId || !route) throw new TypeError('stream consumer route requires message identity and route'); + if (routes.has(messageId)) { + throw new Error(`stream consumer route already registered: ${messageId}`); + } + const token = Object.freeze({}); + const lease = { active: true }; + const ownedRoute = Object.freeze({ + get suppressed() { return !lease.active || route.suppressed === true; }, + start(...args) { if (lease.active) return route.start?.(...args); }, + ...(typeof route.append === 'function' ? { + append(...args) { if (lease.active) return route.append(...args); }, + } : {}), + ...(typeof route.projectTerminal === 'function' ? { + projectTerminal(...args) { if (lease.active) return route.projectTerminal(...args); }, + } : {}), + settle(...args) { return route.settle?.(...args); }, + release(...args) { + if (!lease.active) { + if (routes.get(messageId)?.token === token) routes.delete(messageId); + return; + } + lease.active = false; + try { return route.release?.(...args); } + finally { if (routes.get(messageId)?.token === token) routes.delete(messageId); } + }, + }); + routes.set(messageId, { token, route: ownedRoute, lease }); + const release = () => { + if (routes.get(messageId)?.token !== token) return; + lease.active = false; + routes.delete(messageId); + }; + release.retract = () => { + if (routes.get(messageId)?.token !== token) return; + lease.active = false; + }; + return release; + }, + claim(messageId) { + return routes.get(messageId)?.route || null; + }, + dispose() { + disposed = true; + routes.forEach(entry => { entry.lease.active = false; }); + routes.clear(); + }, + }); +} diff --git a/modules/chat/streamCoordinator.js b/modules/chat/streamCoordinator.js new file mode 100644 index 000000000..f024b7170 --- /dev/null +++ b/modules/chat/streamCoordinator.js @@ -0,0 +1,265 @@ +import { createStreamSession, normalizeStreamTerminal } from './streamSession.js'; + +const freeze = value => Object.freeze(value); +const deferred = () => { + let resolve; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +}; +const errorDto = error => freeze({ + name: String(error?.name || 'Error'), + message: String(error?.message || error || 'Unknown stream error'), +}); +const immutableDto = value => { + if (Array.isArray(value)) return freeze(value.map(immutableDto)); + if (value && typeof value === 'object') { + if (value instanceof Error) return errorDto(value); + const copy = {}; + for (const [key, nested] of Object.entries(value)) copy[key] = immutableDto(nested); + return freeze(copy); + } + return value; +}; + +/** + * Coordinates one-shot stream operations without owning DOM, Electron or UI state. + * `run` is the transport provider; `persist` is the sole durable-result provider. + */ +export function createStreamCoordinator({ run, persist, reportError = console.error } = {}) { + if (typeof run !== 'function') throw new TypeError('stream coordinator requires a run provider'); + if (persist !== undefined && typeof persist !== 'function') throw new TypeError('persist must be a function'); + + const records = new Map(); + const replacements = new Map(); + const generations = new Map(); + const saveQueues = new Map(); + let coordinatorDisposed = false; + + const safeReport = (label, error) => { + try { reportError(label, error); } catch { /* diagnostics cannot own lifecycle */ } + }; + const owns = record => records.get(record.sessionId) === record && record.accepting; + const publish = (record, event) => { + if (!record.surfaceAttached || typeof record.onEvent !== 'function') return; + try { record.onEvent(immutableDto({ + ...event, + sessionId: record.sessionId, + conversationKey: record.conversationKey, + generation: record.generation, + surfaceGeneration: record.surfaceGeneration, + })); } catch (error) { safeReport('[StreamCoordinator] consumer failed', error); } + }; + const enqueuePersistence = (record, task) => { + const previous = saveQueues.get(record.conversationKey) || Promise.resolve(); + const queued = previous.then(task); + const tail = queued.catch(() => {}).finally(() => { + if (saveQueues.get(record.conversationKey) === tail) saveQueues.delete(record.conversationKey); + }); + saveQueues.set(record.conversationKey, tail); + return queued; + }; + const releaseOwnedResources = async record => { + const cleanups = record.cleanups.splice(0).reverse(); + for (const cleanup of cleanups) { + try { await cleanup(); } catch (error) { safeReport('[StreamCoordinator] cleanup failed', error); } + } + }; + + const start = (request = {}, options = {}) => { + if (coordinatorDisposed) throw new Error('stream coordinator is disposed'); + const sessionId = String(request.sessionId || request.messageId || '').trim(); + const conversationKey = String(request.conversationKey || '').trim(); + const replacementKey = String(request.replacementKey || request.messageId || sessionId).trim(); + if (!sessionId || !conversationKey || !replacementKey) { + throw new TypeError('stream start requires session, conversation and replacement identity'); + } + if (options.onEvent !== undefined && typeof options.onEvent !== 'function') { + throw new TypeError('onEvent must be a function'); + } + + const authorityKey = `${conversationKey}\u0000${replacementKey}`; + const previous = replacements.get(authorityKey); + if (previous) previous.replace(); + const generation = (generations.get(authorityKey) || 0) + 1; + generations.set(authorityKey, generation); + + const abortController = new AbortController(); + const completion = deferred(); + const record = { + sessionId, + conversationKey, + replacementKey, + authorityKey, + generation, + request: immutableDto({ ...request, sessionId, conversationKey }), + surfaceGeneration: options.surfaceGeneration, + onEvent: options.onEvent, + surfaceAttached: true, + accepting: true, + disposeReason: null, + abortController, + cleanups: [], + completion, + disposePromise: null, + externalAbortCleanup: null, + session: createStreamSession({ sessionId, conversationKey, generation }), + }; + records.set(sessionId, record); + replacements.set(authorityKey, record); + + const stop = (kind, reason, detachSurface) => { + if (detachSurface) { + record.surfaceAttached = false; + record.onEvent = null; + } + if (!record.accepting) return; + record.disposeReason = reason; + record.accepting = false; + record.session.terminal(kind, { reason }); + abortController.abort(reason); + }; + record.replace = () => stop('discarded', 'replaced', true); + + const controls = freeze({ + signal: abortController.signal, + pushChunk(chunk) { + if (!owns(record) || !record.session.pushChunk(chunk, generation)) return false; + publish(record, { type: 'chunk', text: typeof chunk === 'string' ? chunk : String(chunk?.text || '') }); + return true; + }, + final(payload = {}) { + if (!owns(record) || record.session.snapshot.terminal) return false; + publish(record, { type: 'final', payload: immutableDto(payload) }); + return true; + }, + terminal(input, payload = {}) { + if (!owns(record)) return false; + const terminal = normalizeStreamTerminal(typeof input === 'string' ? { kind: input, ...payload } : input); + return record.session.terminal(terminal.kind, terminal, generation); + }, + own(cleanup) { + if (!owns(record) || typeof cleanup !== 'function') return false; + record.cleanups.push(cleanup); + return true; + }, + }); + + publish(record, { type: 'started' }); + + const work = (async () => { + let runResult; + try { + runResult = await run(record.request, controls); + if (!record.session.snapshot.terminal) { + const terminal = normalizeStreamTerminal(runResult || 'completed'); + record.session.terminal(terminal.kind, terminal, generation); + } + } catch (error) { + if (!record.session.snapshot.terminal) { + const kind = abortController.signal.aborted ? (record.disposeReason === 'replaced' || record.disposeReason === 'disposed' ? 'discarded' : 'cancelled') : 'failed'; + record.session.terminal(kind, { error: errorDto(error), reason: record.disposeReason || undefined }, generation); + } + } + + const transport = await record.session.done; + let persistence = freeze({ status: 'skipped' }); + let persistenceValue; + let outcomeKind = transport.kind; + let phase; + if (persist && transport.kind !== 'discarded' && records.get(sessionId) === record) { + try { + await enqueuePersistence(record, async () => { + if (records.get(sessionId) !== record) return; + persistenceValue = await persist(freeze({ + request: record.request, + terminal: immutableDto(transport), + snapshot: immutableDto(record.session.snapshot), + })); + }); + persistence = immutableDto({ + status: records.get(sessionId) === record ? 'saved' : 'skipped', + value: persistenceValue, + }); + } catch (error) { + persistence = freeze({ status: 'failed', error: errorDto(error) }); + outcomeKind = 'failed'; + phase = 'persistence'; + safeReport('[StreamCoordinator] persistence failed', error); + } + } + + const outcome = immutableDto({ + kind: outcomeKind, + phase, + transport, + persistence, + sessionId, + conversationKey, + generation, + text: record.session.snapshot.text, + chunkCount: record.session.snapshot.chunkCount, + }); + publish(record, { type: outcome.kind, outcome }); + await releaseOwnedResources(record); + record.externalAbortCleanup?.(); + if (records.get(sessionId) === record) records.delete(sessionId); + if (replacements.get(authorityKey) === record) replacements.delete(authorityKey); + completion.resolve(outcome); + return outcome; + })().catch(error => { + safeReport('[StreamCoordinator] internal failure', error); + const outcome = immutableDto({ kind: 'failed', phase: 'coordinator', error: errorDto(error), sessionId, conversationKey, generation }); + completion.resolve(outcome); + return outcome; + }); + record.work = work; + + if (options.signal) { + const externalAbort = () => stop('cancelled', 'external-abort', false); + if (options.signal.aborted) externalAbort(); + else { + options.signal.addEventListener('abort', externalAbort, { once: true }); + record.externalAbortCleanup = () => options.signal.removeEventListener('abort', externalAbort); + } + } + + const handle = freeze({ + sessionId, + conversationKey, + generation, + get done() { return completion.promise; }, + cancel(reason = 'cancelled') { + stop('cancelled', reason, false); + return completion.promise; + }, + dispose(reason = 'disposed') { + if (!record.disposePromise) { + stop('discarded', reason, true); + record.disposePromise = work.then(() => completion.promise); + } + return record.disposePromise; + }, + }); + return handle; + }; + + return freeze({ + start, + async dispose() { + if (coordinatorDisposed) return; + coordinatorDisposed = true; + const active = [...records.values()]; + await Promise.all(active.map(record => { + record.surfaceAttached = false; + record.onEvent = null; + if (record.accepting) { + record.disposeReason = 'coordinator-disposed'; + record.accepting = false; + record.session.terminal('discarded', { reason: record.disposeReason }); + record.abortController.abort(record.disposeReason); + } + return record.work; + })); + }, + }); +} diff --git a/modules/chat/streamSession.js b/modules/chat/streamSession.js new file mode 100644 index 000000000..8c34a4b09 --- /dev/null +++ b/modules/chat/streamSession.js @@ -0,0 +1,85 @@ +/** Pure stream-session protocol; readers, DOM, Electron and persistence stay outside. */ +import { normalizeChatTerminal, STREAM_TERMINAL_KINDS } from './chatEventContract.js'; + +const TERMINALS = new Set(STREAM_TERMINAL_KINDS); +const freeze = value => Object.freeze(value); +const deferred = () => { + let resolve; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +}; + +export function normalizeStreamTerminal(input, fallback = {}) { + try { + return normalizeChatTerminal(input, { ...fallback, ...(fallback.kind ? {} : { kind: undefined }) }); + } catch (error) { + if (fallback.kind) return freeze({ ...fallback, ...(input && typeof input === 'object' ? input : { kind: input }) }); + throw error; + } +} + +export function createStreamSession(request = {}) { + const sessionId = String(request.sessionId || request.messageId || '').trim(); + const conversationKey = String(request.conversationKey || '').trim(); + if (!sessionId || !conversationKey) throw new TypeError('stream session requires id and conversation key'); + const generation = Number.isInteger(request.generation) ? request.generation : 0; + let state = freeze({ status: 'started', sessionId, conversationKey, generation, chunkCount: 0, text: '', terminal: null }); + let disposed = false; + const completion = deferred(); + const listeners = new Set(); + const emit = event => [...listeners].forEach(listener => { + try { listener(freeze({ ...event, sessionId, conversationKey, generation }), state); } + catch (error) { console.error('[StreamSession] consumer failed:', error); } + }); + const update = (next, event) => { + if (disposed) return false; + state = freeze({ ...state, ...next }); + emit(event); + return true; + }; + const api = { + get snapshot() { return state; }, + get done() { return completion.promise; }, + subscribe(listener) { + if (disposed) return () => {}; + if (typeof listener !== 'function') throw new TypeError('stream listener is required'); + listeners.add(listener); + return () => listeners.delete(listener); + }, + pushChunk(chunk, candidateGeneration = generation) { + if (disposed || state.terminal || candidateGeneration !== generation) return false; + const text = typeof chunk === 'string' ? chunk : String(chunk?.text || ''); + if (!text) return false; + return update({ status: 'streaming', chunkCount: state.chunkCount + 1, text: state.text + text }, { type: 'chunk', text }); + }, + terminal(kind, payload = {}, candidateGeneration = generation) { + if (!TERMINALS.has(kind)) throw new Error(`unsupported stream terminal: ${kind}`); + if (disposed || state.terminal || candidateGeneration !== generation) return false; + const terminal = normalizeStreamTerminal({ kind, ...payload }); + const accepted = update({ status: kind, terminal }, { type: kind, terminal }); + if (accepted) completion.resolve(terminal); + return accepted; + }, + discard(reason = 'disposed') { return api.terminal('discarded', { reason }); }, + async dispose(reason = 'disposed') { + if (!disposed) { + if (!state.terminal) api.discard(reason); + disposed = true; + listeners.clear(); + } + return completion.promise; + }, + }; + return api; +} + +export function createStreamStateReducer() { + return (state, event) => { + if (!state || state.sessionId !== event?.sessionId || state.terminal) return state; + if (event.type === 'chunk') return freeze({ ...state, status: 'streaming', text: state.text + event.text, chunkCount: (state.chunkCount || 0) + 1 }); + if (TERMINALS.has(event.type)) return freeze({ ...state, status: event.type, terminal: event.terminal || freeze({ kind: event.type }) }); + return state; + }; +} + +export const STREAM_TERMINALS = Object.freeze([...TERMINALS]); diff --git a/modules/chat/streamTransientHistory.js b/modules/chat/streamTransientHistory.js new file mode 100644 index 000000000..86bdddf3e --- /dev/null +++ b/modules/chat/streamTransientHistory.js @@ -0,0 +1,89 @@ +const cloneHistory = history => history.map(message => ({ ...message })); +const isTemporaryContext = context => ( + context?.topicId === 'assistant_chat' || context?.topicId?.startsWith?.('voicechat_') +); + +/** + * Owns renderer-transient stream message models. This provider never performs + * a durable save; ChatHistoryPersistence remains the sole durable authority. + */ +export function createStreamTransientHistory({ repository, currentHistory } = {}) { + if (typeof repository?.getHistory !== 'function') { + throw new TypeError('StreamTransientHistory requires a history reader'); + } + if (typeof currentHistory?.get !== 'function' || typeof currentHistory?.replace !== 'function') { + throw new TypeError('StreamTransientHistory requires a current Surface history capability'); + } + const pending = new Map(); + let disposed = false; + + const readFor = async (context, visible, messageId = null) => { + const current = cloneHistory(currentHistory.get() || []); + const ownsPending = visible && messageId && current.some(message => message?.id === messageId); + if (visible || ownsPending || isTemporaryContext(context)) return current; + const itemId = context?.groupId || context?.agentId; + const itemType = context?.isGroupMessage ? 'group' : 'agent'; + if (!itemId || !context?.topicId) throw new Error('Transient stream history requires item and topic identity'); + const history = await repository.getHistory(itemId, itemType, context.topicId); + if (!Array.isArray(history)) throw new Error('Transient stream history reader returned an invalid history'); + return cloneHistory(history); + }; + + return Object.freeze({ + async prepare(message, context, { visible = false } = {}) { + if (disposed) throw new Error('StreamTransientHistory is disposed'); + const history = await readFor(context, visible); + const skipThinkingSeed = context?.isGroupMessage === true && message?.isThinking === true; + const placeholder = { + ...message, + content: skipThinkingSeed ? '' : (message?.content || ''), + isThinking: false, + isPendingStream: true, + timestamp: message?.timestamp || Date.now(), + isGroupMessage: context?.isGroupMessage === true, + name: context?.agentName, + agentId: context?.agentId, + }; + pending.set(message.id, { ...placeholder }); + const index = history.findIndex(entry => entry?.id === message.id); + if (index < 0) history.push(placeholder); + else history[index] = { ...history[index], ...placeholder }; + if (visible) currentHistory.replace(cloneHistory(history)); + return history; + }, + async finalize({ messageId, context, content, finishReason, visible = false } = {}) { + if (disposed) return null; + const history = await readFor(context, visible, messageId); + let index = history.findIndex(message => message?.id === messageId); + if (index < 0) { + const placeholder = pending.get(messageId); + if (placeholder) { + const replyIndex = placeholder.replyToMessageId + ? history.findIndex(message => message?.id === placeholder.replyToMessageId) + : -1; + index = replyIndex >= 0 ? replyIndex + 1 : history.length; + history.splice(index, 0, { ...placeholder }); + } + } + if (index < 0) return null; + const message = { + ...history[index], + content, + finishReason, + isThinking: false, + }; + delete message.isPendingStream; + if (message.isGroupMessage) { + message.name = context?.agentName || message.name; + message.agentId = context?.agentId || message.agentId; + } + history[index] = message; + if (visible) currentHistory.replace(cloneHistory(history)); + pending.delete(messageId); + return Object.freeze({ message: Object.freeze({ ...message }), history }); + }, + discard(messageId) { pending.delete(messageId); }, + get pendingCount() { return pending.size; }, + dispose() { disposed = true; pending.clear(); }, + }); +} diff --git a/modules/chat/surfaceConversation.js b/modules/chat/surfaceConversation.js new file mode 100644 index 000000000..3731903bc --- /dev/null +++ b/modules/chat/surfaceConversation.js @@ -0,0 +1,41 @@ +const emptySelection = () => ({ id: null, type: null, name: null, avatarUrl: null, config: null }); + +/** + * Owns the immutable identity and mutable in-memory history for one internal + * chat Surface. This is a narrow operation capability, not an application + * Store: it has no subscription API and cannot switch to another conversation. + */ +export function createSurfaceConversation({ selectedItem = null, topicId = null, history = [] } = {}) { + const ownedSelection = selectedItem ? { ...selectedItem } : emptySelection(); + const ownedTopicId = topicId ?? null; + let ownedHistory = Array.isArray(history) ? [...history] : []; + let active = true; + + const assertActive = () => { + if (!active) throw new Error('Surface conversation is disposed'); + }; + + return Object.freeze({ + selectedItemRef: Object.freeze({ get: () => ownedSelection, set: () => false }), + topicIdRef: Object.freeze({ get: () => ownedTopicId, set: () => false }), + historyRef: Object.freeze({ + get: () => ownedHistory, + set(value) { + if (!active) return false; + ownedHistory = Array.isArray(value) ? [...value] : []; + return true; + }, + }), + isActive: () => active, + replaceHistory(value) { + assertActive(); + ownedHistory = Array.isArray(value) ? [...value] : []; + return ownedHistory; + }, + dispose() { + if (!active) return; + active = false; + ownedHistory = []; + }, + }); +} diff --git a/modules/chat/vcpStreamBridge.js b/modules/chat/vcpStreamBridge.js new file mode 100644 index 000000000..209b5274d --- /dev/null +++ b/modules/chat/vcpStreamBridge.js @@ -0,0 +1,164 @@ +import { createStreamCoordinator } from './streamCoordinator.js'; + +const MANAGED_TYPES = new Set(['agent_thinking', 'start', 'data', 'end', 'error']); +// VCP message ids are one-shot. Keep a bounded recent terminal cache so delayed +// IPC cannot resurrect an operation without retaining every id for the app lifetime. +const MAX_RETIRED_SESSIONS = 2048; + +/** Adapts preload's externally-produced VCP stream events to owner-scoped handles. */ +export function createVcpStreamBridge({ createConsumer, reportError = console.error } = {}) { + if (typeof createConsumer !== 'function') throw new TypeError('VCP stream bridge requires a consumer factory'); + const operations = new Map(); + const consumers = new Map(); + const messageOperations = new Map(); + const retiredSessions = new Set(); + let disposed = false; + const retireSession = sessionId => { + if (!sessionId) return; + retiredSessions.delete(sessionId); + retiredSessions.add(sessionId); + while (retiredSessions.size > MAX_RETIRED_SESSIONS) { + retiredSessions.delete(retiredSessions.values().next().value); + } + }; + + const coordinator = createStreamCoordinator({ + reportError, + run: (request, controls) => new Promise(resolve => { + const operation = { + controls, + resolve, + chain: Promise.resolve(), + handle: null, + disposePromise: null, + cancelPromise: null, + accepting: true, + projecting: true, + }; + operations.set(request.sessionId, operation); + controls.signal.addEventListener('abort', () => { + if (operations.get(request.sessionId) === operation) resolve({ kind: 'discarded', reason: controls.signal.reason }); + }, { once: true }); + }), + persist: value => consumers.get(value.request.sessionId)?.persist?.(value), + }); + + const ensureOperation = event => { + if (disposed) return null; + const messageId = String(event?.messageId || '').trim(); + const sessionId = String(event?.streamOperationId || messageId).trim(); + if (!messageId || !sessionId || retiredSessions.has(sessionId)) return null; + let operation = operations.get(sessionId); + if (operation) return operation; + + const context = event.context || {}; + const conversationKey = context.groupId + ? `group:${context.groupId}/topic:${context.topicId || ''}` + : `agent:${context.agentId || ''}/topic:${context.topicId || ''}`; + const consumer = createConsumer(event); + consumers.set(sessionId, consumer); + messageOperations.set(messageId, sessionId); + const handle = coordinator.start({ + sessionId, + messageId, + conversationKey, + context, + }, { + surfaceGeneration: consumer.surfaceGeneration, + onEvent: streamEvent => consumer.consume?.(streamEvent), + }); + handle.done.finally(() => { + retireSession(sessionId); + consumer.dispose?.(); + if (operations.get(sessionId)?.handle === handle) operations.delete(sessionId); + if (consumers.get(sessionId) === consumer) consumers.delete(sessionId); + if (messageOperations.get(messageId) === sessionId) messageOperations.delete(messageId); + }); + operation = operations.get(sessionId); + operation.handle = handle; + return operation; + }; + + const disposeOperation = (messageId, reason = 'surface-disposed') => { + const visibleId = String(messageId || '').trim(); + const sessionId = messageOperations.get(visibleId) || visibleId; + retireSession(sessionId); + const operation = operations.get(sessionId); + if (!operation) return Promise.resolve(false); + if (!operation.disposePromise) { + operation.accepting = false; + operation.projecting = false; + operation.disposePromise = (async () => { + const outcome = await operation.handle.dispose(reason); + await Promise.allSettled([operation.chain]); + return outcome; + })(); + } + return operation.disposePromise; + }; + const cancelOperation = (messageId, reason = 'cancelled') => { + const visibleId = String(messageId || '').trim(); + const sessionId = messageOperations.get(visibleId) || visibleId; + const operation = operations.get(sessionId); + if (!operation) { + retireSession(sessionId); + return Promise.resolve(null); + } + if (!operation.cancelPromise) { + operation.accepting = false; + operation.cancelPromise = operation.chain.then(() => operation.handle.cancel(reason)); + } + return operation.cancelPromise; + }; + + return Object.freeze({ + accept(event) { + if (!MANAGED_TYPES.has(event?.type) || !event?.messageId) return false; + const operation = ensureOperation(event); + if (!operation?.accepting) return false; + const consumer = consumers.get(String(event.streamOperationId || event.messageId)); + operation.chain = operation.chain.then(async () => { + if (!operation.projecting) return; + await consumer?.prepare?.(event); + if (!operation.projecting) return; + if (event.type === 'data') { + const normalized = consumer?.normalizeChunk?.(event.chunk) ?? event.chunk; + operation.controls.pushChunk(normalized); + } + if (event.type === 'end' || event.type === 'error') { + const terminal = event.type === 'end' + ? { kind: event.finish_reason || 'completed', fullResponse: event.fullResponse, context: event.context } + : { kind: 'failed', error: event.error, fullResponse: event.fullResponse || event.accumulatedResponse, context: event.context }; + operation.accepting = false; + operation.projecting = false; + operation.controls.terminal(terminal); + operation.resolve(terminal); + } + }).catch(error => { + operation.accepting = false; + operation.projecting = false; + reportError('[VcpStreamBridge] event projection failed', error); + const terminal = { kind: 'failed', phase: 'projection', error }; + operation.controls.terminal(terminal); + operation.resolve(terminal); + }); + return true; + }, + cancelOperation, + disposeOperation, + async dispose() { + if (disposed) return; + disposed = true; + operations.forEach(operation => { + operation.accepting = false; + operation.projecting = false; + }); + await coordinator.dispose(); + await Promise.allSettled([...operations.values()].map(operation => operation.chain)); + operations.clear(); + consumers.clear(); + messageOperations.clear(); + retiredSessions.clear(); + }, + }); +} diff --git a/modules/chatManager.js b/modules/chatManager.js index 3bc11150f..c120c83ce 100755 --- a/modules/chatManager.js +++ b/modules/chatManager.js @@ -1,37 +1,76 @@ // modules/chatManager.js -window.chatManager = (() => { +export const chatManager = (() => { // --- Private Variables --- let electronAPI; let uiHelper; - let messageRenderer; + let messageRenderer; + let chatDomRenderer; let itemListManager; let topicListManager; - let groupRenderer; + let groupRenderer; + let streamProjection; + let interruptCapability; // References to state in renderer.js let currentSelectedItemRef; let currentTopicIdRef; let currentChatHistoryRef; let attachedFilesRef; - let globalSettingsRef; + let globalSettingsRef; + let chatContext; + let chatRepository; + let historyMutationAuthority; + let streamConsumerRegistry; + + function requireHistoryRepository() { + if (!chatRepository) throw new Error('ChatRepository is required for chat history operations'); + return chatRepository; + } + + function getHistory(itemId, itemType, topicId) { + const repository = requireHistoryRepository(); + return repository.getHistory(itemId, itemType, topicId); + } + + function saveHistory(itemId, itemType, topicId, history) { + if (historyMutationAuthority) { + return historyMutationAuthority.replace({ + itemId, + itemType, + topicId, + category: 'chat-manager', + }, history).then(commit => commit.result || { success: true }); + } + const repository = requireHistoryRepository(); + return repository.saveHistory(itemId, itemType, topicId, history); + } // DOM Elements from renderer.js let elements = {}; // Functions from main renderer - let mainRendererFunctions = {}; + let mainRendererFunctions = {}; + // Narrow capability supplied by the owning Surface. ChatManager must not + // discover the main-window send button through the ambient window object. + let notifySendStateChanged = () => {}; let isCanvasWindowOpen = false; // State to track if the canvas window is open let lastAssistantSuspendAt = 0; - let activeHistoryLoadToken = 0; - let itemSelectionGeneration = 0; + let activeHistoryLoadToken = 0; + let historySyncGeneration = 0; + let itemSelectionGeneration = 0; let topicSelectionGeneration = 0; let topicCreationGeneration = 0; let pendingItemSelectionToken = null; - let emptyStateObserver = null; + let emptyStateObserver = null; + let canvasContentDisposer = null; + let canvasClosedDisposer = null; + const forwardTimers = new Set(); const outgoingPersistenceQueues = new Map(); const pendingSendContexts = new Set(); let lastOpenSaveQueue = Promise.resolve(); + let initialized = false; + let disposed = false; function insertAfterMessage(history, ownerMessageId, message) { const next = Array.isArray(history) ? [...history] : []; @@ -47,17 +86,18 @@ window.chatManager = (() => { const signature = `${sendContext.agentId}:${sendContext.topicId}`; const previous = outgoingPersistenceQueues.get(signature) || Promise.resolve(); const operation = previous.catch(() => {}).then(async () => { - const persisted = await electronAPI.getChatHistory(sendContext.agentId, sendContext.topicId); + const persisted = await getHistory(sendContext.agentId, sendContext.itemType || 'agent', sendContext.topicId); if (!Array.isArray(persisted)) { throw new Error(persisted?.error || '读取聊天记录失败'); } const nextHistory = persisted.some(message => message?.id === userMessage.id) ? persisted : [...persisted, userMessage]; - const saveResult = await electronAPI.saveChatHistory( - sendContext.agentId, - sendContext.topicId, - nextHistory + const saveResult = await saveHistory( + sendContext.agentId, + sendContext.itemType || 'agent', + sendContext.topicId, + nextHistory ); if (saveResult?.success === false) { throw new Error(saveResult.error || '保存聊天记录失败'); @@ -319,7 +359,15 @@ window.chatManager = (() => { * Initializes the ChatManager module. * @param {object} config - The configuration object. */ - function init(config) { + function init(config) { + if (disposed) throw new Error('ChatManager has been disposed'); + initialized = false; + chatContext = config.chatContext || null; + chatRepository = config.chatRepository || null; + historyMutationAuthority = config.historyMutationAuthority || null; + streamConsumerRegistry = config.streamConsumerRegistry || null; + if (!chatRepository) throw new Error('ChatManager requires ChatRepository'); + chatDomRenderer = config.chatDomRenderer || null; electronAPI = config.electronAPI; uiHelper = config.uiHelper; @@ -327,7 +375,9 @@ window.chatManager = (() => { messageRenderer = config.modules.messageRenderer; itemListManager = config.modules.itemListManager; topicListManager = config.modules.topicListManager; - groupRenderer = config.modules.groupRenderer; + groupRenderer = config.modules.groupRenderer; + streamProjection = config.modules.streamManager || null; + interruptCapability = config.modules.interruptHandler || null; // State References currentSelectedItemRef = config.refs.currentSelectedItemRef; @@ -354,15 +404,34 @@ window.chatManager = (() => { } // Main Renderer Functions - mainRendererFunctions = config.mainRendererFunctions; - - console.log('[ChatManager] Initialized successfully.'); - - // Listen for Canvas events - if (electronAPI) { - electronAPI.onCanvasContentUpdate(handleCanvasContentUpdate); - electronAPI.onCanvasWindowClosed(handleCanvasWindowClosed); - } + mainRendererFunctions = config.mainRendererFunctions; + notifySendStateChanged = typeof config.notifySendStateChanged === 'function' + ? config.notifySendStateChanged + : (typeof mainRendererFunctions?.updateSendButtonState === 'function' + ? mainRendererFunctions.updateSendButtonState + : () => {}); + + // Listen for Canvas events + if (electronAPI) { + canvasContentDisposer?.(); + canvasClosedDisposer?.(); + canvasContentDisposer = null; + canvasClosedDisposer = null; + try { + canvasContentDisposer = electronAPI.onCanvasContentUpdate?.(handleCanvasContentUpdate) || null; + canvasClosedDisposer = electronAPI.onCanvasWindowClosed?.(handleCanvasWindowClosed) || null; + } catch (error) { + canvasContentDisposer?.(); + canvasClosedDisposer?.(); + canvasContentDisposer = null; + canvasClosedDisposer = null; + emptyStateObserver?.disconnect(); + emptyStateObserver = null; + throw error; + } + } + initialized = true; + console.log('[ChatManager] Initialized successfully.'); } /** @@ -549,15 +618,17 @@ window.chatManager = (() => { let currentSelectedItem = currentSelectedItemRef.get(); currentSelectedItem = { id: itemId, type: itemType, name: itemName, avatarUrl: itemAvatarUrl, config: itemFullConfig }; - currentSelectedItemRef.set(currentSelectedItem); + currentSelectedItemRef.set(currentSelectedItem); + chatContext?.setSelectedItem(currentSelectedItem); // From this point displayNoItemSelected can rely on the selected item // itself. Keep generation ownership for the async transaction, but do // not retain a separate pending marker that an unrelated renderer // exception could strand forever. finishSelection(); currentTopicIdRef.set(null); // Reset topic - currentChatHistoryRef.set([]); - window.updateSendButtonState?.(); + currentChatHistoryRef.set([]); + chatContext?.setHistory([]); + notifySendStateChanged(); document.querySelectorAll('.topic-list .topic-item.active-topic-glowing').forEach(item => { item.classList.remove('active-topic-glowing'); @@ -677,7 +748,8 @@ window.chatManager = (() => { sendMessageBtn.disabled = false; attachFileBtn.disabled = false; // messageInput.focus(); - if (topicListManager) topicListManager.loadTopicList(); + if (topicListManager) await Promise.resolve(topicListManager.loadTopicList()); + if (!isSelectionCurrent()) return; await _saveLastOpenState(); // Commit before startup/reload can observe the selection. finishSelection(); } @@ -755,10 +827,14 @@ window.chatManager = (() => { }; let watcherLeaseToken = null; - try { - currentTopicIdRef.set(topicId); - if (messageRenderer) messageRenderer.setCurrentTopicId(topicId); - const lease = await beginHistoryWatcherOperation(); + try { + currentTopicIdRef.set(topicId); + if (messageRenderer) messageRenderer.setCurrentTopicId(topicId); + // Persist the selection intent before watcher/history work. A + // renderer reload or crash during that work must restore the + // topic the user actually selected, not the previous durable one. + await _saveLastOpenState(); + const lease = await beginHistoryWatcherOperation(); if (!isTopicSelectionCurrent() || lease?.stale || lease?.success === false) return; watcherLeaseToken = lease.token || null; // Persist the user's selection intent at the same point as the @@ -795,7 +871,7 @@ window.chatManager = (() => { } } - async function handleTopicDeletion(remainingTopics, deletionContext = null) { + async function handleTopicDeletion(remainingTopics, deletionContext = null) { let currentSelectedItem = currentSelectedItemRef.get(); if ( deletionContext @@ -807,12 +883,25 @@ window.chatManager = (() => { console.debug('[ChatManager] Ignoring stale topic deletion completion', deletionContext); return false; } - const config = currentSelectedItem.config || currentSelectedItem; - config.topics = remainingTopics; - currentSelectedItemRef.set(currentSelectedItem); - - if (remainingTopics && remainingTopics.length > 0) { - const newSelectedTopic = remainingTopics.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))[0]; + const deletedTopicIds = new Set([ + ...(Array.isArray(deletionContext?.deletedTopicIds) ? deletionContext.deletedTopicIds : []), + ...(deletionContext?.topicId ? [deletionContext.topicId] : []), + ].filter(Boolean).map(String)); + const sanitizedRemainingTopics = (Array.isArray(remainingTopics) ? remainingTopics : []) + .filter(topic => !deletedTopicIds.has(String(topic?.id))); + const currentConfig = currentSelectedItem.config || currentSelectedItem; + const nextConfig = { ...currentConfig, topics: sanitizedRemainingTopics }; + currentSelectedItem = currentSelectedItem.config + ? { ...currentSelectedItem, config: nextConfig } + : { ...currentSelectedItem, ...nextConfig }; + currentSelectedItemRef.set(currentSelectedItem); + + if (sanitizedRemainingTopics.length > 0) { + const fallbackTopic = deletionContext?.fallbackTopicId + ? sanitizedRemainingTopics.find(topic => String(topic?.id) === String(deletionContext.fallbackTopicId)) + : null; + const newSelectedTopic = fallbackTopic || [...sanitizedRemainingTopics] + .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))[0]; await selectTopic(newSelectedTopic.id); } else { ++topicSelectionGeneration; @@ -846,7 +935,7 @@ window.chatManager = (() => { if (messageRenderer) messageRenderer.clearChat(); currentChatHistoryRef.set([]); - window.updateSendButtonState?.(); + notifySendStateChanged(); document.querySelectorAll('.topic-list .topic-item').forEach(item => { @@ -887,12 +976,7 @@ window.chatManager = (() => { return; } - let historyResult; - if (itemType === 'agent') { - historyResult = await electronAPI.getChatHistory(itemId, topicId); - } else if (itemType === 'group') { - historyResult = await electronAPI.getGroupChatHistory(itemId, topicId); - } + const historyResult = await getHistory(itemId, itemType, topicId); if (abortIfStale()) { if (messageRenderer) messageRenderer.removeMessageById('loading_history'); @@ -921,7 +1005,7 @@ window.chatManager = (() => { if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载话题 "${topicId}" 的聊天记录失败: ${historyResult.error}`, timestamp: Date.now() }); } else if (historyResult && historyResult.length > 0) { currentChatHistoryRef.set(historyResult); - window.updateSendButtonState?.(); + notifySendStateChanged(); if (messageRenderer) { // 使用优化的分批渲染策略 const renderOptions = { @@ -931,14 +1015,14 @@ window.chatManager = (() => { }; console.log(`[ChatManager] 开始加载话题历史,共 ${historyResult.length} 条消息`); - await messageRenderer.renderHistory(historyResult, renderOptions); + await (chatDomRenderer || messageRenderer).renderHistory(historyResult, renderOptions); if (abortIfStale()) return; console.log(`[ChatManager] 话题历史加载完成`); } } else if (historyResult) { // History is empty currentChatHistoryRef.set([]); - window.updateSendButtonState?.(); + notifySendStateChanged(); const activeItem = currentSelectedItemRef.get(); if ( activeItem?.id === itemId @@ -951,9 +1035,14 @@ window.chatManager = (() => { if (messageRenderer) messageRenderer.renderMessage({ role: 'system', content: `加载话题 "${topicId}" 的聊天记录时返回了无效数据。`, timestamp: Date.now() }); } - if (abortIfStale()) return; - - if (itemId && topicId && !(historyResult && historyResult.error)) { + if (abortIfStale()) return; + + // Re-project renderer-local active streams after history/DOM replacement. + // This never persists partial output; terminal persistence remains coordinator-owned. + await streamProjection?.reconcileConversation?.({ itemType, itemId, topicId }); + if (abortIfStale()) return; + + if (itemId && topicId && !(historyResult && historyResult.error)) { localStorage.setItem(`lastActiveTopic_${itemId}_${itemType}`, topicId); } } @@ -997,7 +1086,7 @@ window.chatManager = (() => { } try { - await electronAPI.saveChatHistory(currentSelectedItem.id, currentTopicId, updatedHistory); + await saveHistory(currentSelectedItem.id, currentSelectedItem.type, currentTopicId, updatedHistory); currentChatHistoryRef.set(updatedHistory); if (messageRenderer && typeof messageRenderer.updateMessageUI === 'function') { @@ -1098,7 +1187,7 @@ window.chatManager = (() => { if (!message.attachments) message.attachments = []; message.attachments.push(...successfulAttachments); - await electronAPI.saveChatHistory(currentSelectedItem.id, currentTopicId, updatedHistory); + await saveHistory(currentSelectedItem.id, currentSelectedItem.type, currentTopicId, updatedHistory); currentChatHistoryRef.set(updatedHistory); if (messageRenderer && typeof messageRenderer.updateMessageUI === 'function') { @@ -1194,12 +1283,10 @@ window.chatManager = (() => { return; } // 使用最新的配置更新内存中的状态,以保持同步 - if (currentSelectedItem.config) { - currentSelectedItem.config = agentConfigForSummary; - } else { - Object.assign(currentSelectedItem, agentConfigForSummary); - } - currentSelectedItemRef.set(currentSelectedItem); + const refreshedSelectedItem = currentSelectedItem.config + ? { ...currentSelectedItem, name: agentConfigForSummary.name || currentSelectedItem.name, config: agentConfigForSummary } + : { ...currentSelectedItem, ...agentConfigForSummary }; + currentSelectedItemRef.set(refreshedSelectedItem); const topics = agentConfigForSummary.topics || []; const currentTopicObject = topics.find(t => t.id === currentTopicId); @@ -1232,43 +1319,62 @@ window.chatManager = (() => { } } - async function handleSendMessage() { - const { messageInput } = elements; - let content = messageInput.value; // Use let as it might be modified - const attachedFiles = attachedFilesRef.get(); - const currentSelectedItem = currentSelectedItemRef.get(); - const currentTopicId = currentTopicIdRef.get(); - const globalSettings = globalSettingsRef.get(); + async function handleSendMessage(request = null) { + const { messageInput } = elements; + const renderTarget = request?.domRenderer || messageRenderer; + const input = request?.input || messageInput; + let content = typeof request?.content === 'string' ? request.content : input.value; // Use let as it might be modified + const attachedFiles = Array.isArray(request?.attachments) ? request.attachments : attachedFilesRef.get(); + const sendSelectedItemRef = request?.conversation?.selectedItemRef || currentSelectedItemRef; + const sendTopicIdRef = request?.conversation?.topicIdRef || currentTopicIdRef; + const sendHistoryRef = request?.conversation?.historyRef || currentChatHistoryRef; + const currentSelectedItem = sendSelectedItemRef.get(); + const currentTopicId = sendTopicIdRef.get(); + const globalSettings = globalSettingsRef.get(); + const notifySendState = request?.conversation ? () => {} : notifySendStateChanged; const sendContext = { - agentId: currentSelectedItem.id, + agentId: currentSelectedItem.id, + itemType: currentSelectedItem.type || 'agent', agentName: currentSelectedItem.name || currentSelectedItem.id, topicId: currentTopicId, - isGroupMessage: false, + isGroupMessage: currentSelectedItem.type === 'group', avatarUrl: currentSelectedItem.avatarUrl, avatarColor: (currentSelectedItem.config || currentSelectedItem)?.avatarCalculatedColor - }; - const isSendContextCurrent = () => { - const activeItem = currentSelectedItemRef.get(); - return activeItem?.id === sendContext.agentId - && activeItem?.type === 'agent' - && currentTopicIdRef.get() === sendContext.topicId; - }; - - if (!content && attachedFiles.length === 0) return; - if (!currentSelectedItem.id || !currentTopicId) { - uiHelper.showToastNotification('请先选择一个项目和话题。', 'error'); - return; - } - if (!globalSettings.vcpServerUrl) { - uiHelper.showToastNotification('请先在全局设置中配置 VCP 服务器 URL。', 'error'); - uiHelper.openModal('globalSettingsModal'); - return; - } - - setNextUiEmptyStateActive(false); - - if (currentSelectedItem.type === 'group') { - if (groupRenderer && typeof groupRenderer.handleSendGroupMessage === 'function') { + }; + const isSendContextCurrent = () => { + if (request?.conversation && request.conversation.isActive?.() === false) return false; + const activeItem = sendSelectedItemRef.get(); + return activeItem?.id === sendContext.agentId + && activeItem?.type === sendContext.itemType + && sendTopicIdRef.get() === sendContext.topicId; + }; + + if (!content && attachedFiles.length === 0) return; + if (!currentSelectedItem.id || !currentTopicId) { + const error = new Error('请先选择一个项目和话题。'); + if (!request?.conversation) uiHelper.showToastNotification(error.message, 'error'); + if (request?.propagateError) throw error; + return; + } + if (!globalSettings.vcpServerUrl) { + const error = new Error('请先在全局设置中配置 VCP 服务器 URL。'); + if (!request?.conversation) { + uiHelper.showToastNotification(error.message, 'error'); + uiHelper.openModal('globalSettingsModal'); + } + if (request?.propagateError) throw error; + return; + } + + if (!request?.conversation) setNextUiEmptyStateActive(false); + + if (currentSelectedItem.type === 'group') { + if (request?.conversation) { + const error = new Error('独立群聊尚未接入 Surface-owned 群组 operation'); + if (request.propagateError) throw error; + return; + } + if (groupRenderer && typeof groupRenderer.handleSendGroupMessage === 'function') { groupRenderer.handleSendGroupMessage( currentSelectedItem.id, currentTopicId, @@ -1278,10 +1384,12 @@ window.chatManager = (() => { } else { uiHelper.showToastNotification("群聊功能模块未加载,无法发送消息。", 'error'); } - messageInput.value = ''; - attachedFilesRef.set([]); - if(mainRendererFunctions.updateAttachmentPreview) mainRendererFunctions.updateAttachmentPreview(); - uiHelper.autoResizeTextarea(messageInput); + if (!request) { + messageInput.value = ''; + attachedFilesRef.set([]); + if(mainRendererFunctions.updateAttachmentPreview) mainRendererFunctions.updateAttachmentPreview(); + uiHelper.autoResizeTextarea(messageInput); + } // messageInput.focus(); return; } @@ -1335,13 +1443,13 @@ window.chatManager = (() => { attachments: uiAttachments }; - const optimisticHistory = [...currentChatHistoryRef.get(), userMessage]; - if (isSendContextCurrent()) { - currentChatHistoryRef.set(optimisticHistory); + const optimisticHistory = [...sendHistoryRef.get(), userMessage]; + if (isSendContextCurrent()) { + sendHistoryRef.set(optimisticHistory); } let userMessageItem = null; - if (messageRenderer) { - userMessageItem = await messageRenderer.renderMessage(userMessage); + if (renderTarget) { + userMessageItem = await renderTarget.renderMessage(userMessage); } if (!isSendContextCurrent()) { // renderMessage targets the shared chat container. If selection @@ -1358,16 +1466,17 @@ window.chatManager = (() => { // The draft is consumed only after the durable write succeeds. If // persistence fails, retract the optimistic projection and leave // the initiating input/attachments available for retry. - if (isSendContextCurrent()) { - currentChatHistoryRef.set( - currentChatHistoryRef.get().filter(message => message?.id !== userMessage.id) - ); - userMessageItem?.remove?.(); - window.updateSendButtonState?.(); - uiHelper.showToastNotification(`发送失败,草稿已保留: ${error.message}`, 'error'); - } - console.error('[ChatManager] Failed to persist outgoing message:', error); - return; + if (isSendContextCurrent()) { + sendHistoryRef.set( + sendHistoryRef.get().filter(message => message?.id !== userMessage.id) + ); + userMessageItem?.remove?.(); + notifySendState(); + if (!request?.conversation) uiHelper.showToastNotification(`发送失败,草稿已保留: ${error.message}`, 'error'); + } + console.error('[ChatManager] Failed to persist outgoing message:', error); + if (request?.propagateError) throw error; + return; } // Consume only the exact draft transaction that was durably saved. @@ -1375,14 +1484,16 @@ window.chatManager = (() => { // attachment added while the save was pending cannot be discarded. if ( isSendContextCurrent() - && messageInput.value === content - && attachedFilesRef.get() === attachedFiles + && input.value === content + && (request || attachedFilesRef.get() === attachedFiles) ) { - messageInput.value = ''; - attachedFilesRef.set([]); - if (mainRendererFunctions.updateAttachmentPreview) mainRendererFunctions.updateAttachmentPreview(); - if (isCanvasWindowOpen) messageInput.value = CANVAS_PLACEHOLDER; - uiHelper.autoResizeTextarea(messageInput); + if (!request) { + messageInput.value = ''; + attachedFilesRef.set([]); + if (mainRendererFunctions.updateAttachmentPreview) mainRendererFunctions.updateAttachmentPreview(); + if (isCanvasWindowOpen) messageInput.value = CANVAS_PLACEHOLDER; + uiHelper.autoResizeTextarea(messageInput); + } } // 用户已参与该话题:同步清除 TopicSponsor/手动设置的持久化未读标记。 @@ -1423,51 +1534,60 @@ window.chatManager = (() => { avatarColor: (currentSelectedItem.config || currentSelectedItem)?.avatarCalculatedColor }; - let thinkingMessageItem = null; - if (messageRenderer && isSendContextCurrent()) { - thinkingMessageItem = await messageRenderer.renderMessage(thinkingMessage); + let thinkingMessageItem = null; + let releaseStreamConsumerRoute = null; + let settleOwnedStreamOperation = null; + const ownedStreamTerminal = request?.awaitTerminal + ? new Promise(resolve => { settleOwnedStreamOperation = resolve; }) + : null; + if (renderTarget && isSendContextCurrent()) { + thinkingMessageItem = await renderTarget.renderMessage(thinkingMessage); if (!isSendContextCurrent()) { thinkingMessageItem?.remove?.(); thinkingMessageItem = null; } } if (isSendContextCurrent()) { - currentChatHistoryRef.set(insertAfterMessage( - currentChatHistoryRef.get(), + sendHistoryRef.set(insertAfterMessage( + sendHistoryRef.get(), userMessage.id, thinkingMessage )); - window.updateSendButtonState?.(); + notifySendState(); } - const removeThinkingFromSource = async () => { - messageRenderer?.discardStreamingMessage?.(thinkingMessage.id); + const removeThinkingFromSource = async () => { + releaseStreamConsumerRoute?.(); + releaseStreamConsumerRoute = null; + renderTarget?.discardStreaming?.(thinkingMessage.id); try { - const sourceHistory = await electronAPI.getChatHistory(sendContext.agentId, sendContext.topicId); + const sourceHistory = await getHistory(sendContext.agentId, sendContext.itemType || 'agent', sendContext.topicId); if (!Array.isArray(sourceHistory)) return null; const cleanedHistory = sourceHistory.filter(message => message.id !== thinkingMessage.id); if (cleanedHistory.length !== sourceHistory.length) { - const saveResult = await electronAPI.saveChatHistory(sendContext.agentId, sendContext.topicId, cleanedHistory); + const saveResult = await saveHistory(sendContext.agentId, sendContext.itemType || 'agent', sendContext.topicId, cleanedHistory); if (saveResult?.success === false) { throw new Error(saveResult.error || '清理临时消息失败'); } } - if (isSendContextCurrent()) { - currentChatHistoryRef.set( - currentChatHistoryRef.get().filter(message => message?.id !== thinkingMessage.id) - ); - messageRenderer?.removeMessageById?.(thinkingMessage.id); - window.updateSendButtonState?.(); - } + if (isSendContextCurrent()) { + sendHistoryRef.set( + sendHistoryRef.get().filter(message => message?.id !== thinkingMessage.id) + ); + if (typeof renderTarget?.removeMessage === 'function') await renderTarget.removeMessage(thinkingMessage.id); + else await renderTarget?.removeMessageById?.(thinkingMessage.id); + notifySendState(); + } return cleanedHistory; } catch (cleanupError) { console.error('[ChatManager] Failed to clean owned thinking message:', cleanupError); - if (isSendContextCurrent()) { - currentChatHistoryRef.set( - currentChatHistoryRef.get().filter(message => message?.id !== thinkingMessage.id) - ); - messageRenderer?.removeMessageById?.(thinkingMessage.id); - window.updateSendButtonState?.(); - } + if (isSendContextCurrent()) { + sendHistoryRef.set( + sendHistoryRef.get().filter(message => message?.id !== thinkingMessage.id) + ); + if (typeof renderTarget?.removeMessage === 'function') await renderTarget.removeMessage(thinkingMessage.id); + else await renderTarget?.removeMessageById?.(thinkingMessage.id); + notifySendState(); + } return null; } }; @@ -1734,12 +1854,34 @@ window.chatManager = (() => { stream: useStreaming }; - if (useStreaming) { - if (messageRenderer) { - await new Promise(resolve => setTimeout(resolve, 500)); - // Pass the created DOM element directly to avoid race conditions with querySelector - await messageRenderer.startStreamingMessage({ ...thinkingMessage, content: "" }, thinkingMessageItem); - } + if (useStreaming) { + if (messageRenderer) { + releaseStreamConsumerRoute = streamConsumerRegistry?.register?.(thinkingMessage.id, { + kind: request?.domRenderer ? 'independent-surface' : 'main-chat', + start: message => (renderTarget.startStreaming || messageRenderer.startStreamingMessage).call(renderTarget, message, thinkingMessageItem), + ...(request?.domRenderer ? { + append: (messageId, chunk, streamContext) => request.domRenderer.appendStreaming(messageId, chunk, streamContext), + projectTerminal: (messageId, finishReason, streamContext, payload) => request.domRenderer.projectStreamTerminal(messageId, finishReason, streamContext, payload), + } : {}), + settle: result => settleOwnedStreamOperation?.(result), + release: () => { + releaseStreamConsumerRoute?.(); + releaseStreamConsumerRoute = null; + }, + }); + if (request?.domRenderer?.own && releaseStreamConsumerRoute?.retract) { + request.domRenderer.own(() => releaseStreamConsumerRoute?.retract?.()); + } + request?.onOperation?.(Object.freeze({ + messageId: thinkingMessage.id, + done: ownedStreamTerminal, + async cancel(reason) { + try { await interruptCapability?.interrupt?.(thinkingMessage.id); } + catch (error) { console.warn('[ChatManager] Surface interrupt request failed; cancelling locally:', error); } + return releaseStreamConsumerRoute?.cancel?.(reason || 'surface-operation-cancelled'); + }, + })); + } } const context = sendContext; @@ -1757,27 +1899,30 @@ window.chatManager = (() => { if (!useStreaming) { const response = vcpResponse?.response ?? vcpResponse; const responseContext = vcpResponse?.context ?? context; - const activeSelectedItem = currentSelectedItemRef.get(); - const activeTopicId = currentTopicIdRef.get(); + const activeSelectedItem = sendSelectedItemRef.get(); + const activeTopicId = sendTopicIdRef.get(); // Determine if the response is for the currently active chat const isForActiveChat = responseContext && responseContext.agentId === activeSelectedItem.id && responseContext.topicId === activeTopicId; - if (isForActiveChat) { - // If it's for the active chat, update the UI as usual - if (messageRenderer) messageRenderer.removeMessageById(thinkingMessage.id); - } + if (isForActiveChat) { + // Remove the placeholder through the initiating Surface; + // an internal Surface must never mutate the main root. + if (typeof renderTarget?.removeMessage === 'function') await renderTarget.removeMessage(thinkingMessage.id); + else renderTarget?.removeMessageById?.(thinkingMessage.id); + } if (!response) { throw new Error('VCP returned an empty response.'); } - if (response.error) { + if (response.error) { await removeThinkingFromSource(); - if (isForActiveChat && messageRenderer) { - messageRenderer.renderMessage({ role: 'system', content: `VCP错误: ${response.error}`, timestamp: Date.now() }); - } - console.error(`[ChatManager] VCP Error for background message:`, response.error); + if (isForActiveChat && renderTarget) { + renderTarget.renderMessage({ role: 'system', content: `VCP错误: ${response.error}`, timestamp: Date.now() }); + } + console.error(`[ChatManager] VCP Error for background message:`, response.error); + if (request?.propagateError) throw new Error(String(response.error)); } else if (response.choices && response.choices.length > 0) { const assistantMessageContent = response.choices[0].message.content; const assistantMessage = { @@ -1791,21 +1936,21 @@ window.chatManager = (() => { }; // Fetch the correct history from the file, update it, and save it back. - const historyForSave = await electronAPI.getChatHistory(responseContext.agentId, responseContext.topicId); + const historyForSave = await getHistory(responseContext.agentId, responseContext.itemType || 'agent', responseContext.topicId); if (historyForSave && !historyForSave.error) { // Remove any lingering 'thinking' message and add the new one const finalHistory = historyForSave.filter(msg => msg.id !== thinkingMessage.id); finalHistory.push(assistantMessage); // Save the final, complete history to the correct file - await electronAPI.saveChatHistory(responseContext.agentId, responseContext.topicId, finalHistory); + await saveHistory(responseContext.agentId, responseContext.itemType || 'agent', responseContext.topicId, finalHistory); if (isForActiveChat) { // If it's the active chat, also update the UI and in-memory state - currentChatHistoryRef.set(finalHistory); - window.updateSendButtonState?.(); - if (messageRenderer) messageRenderer.renderMessage(assistantMessage); - await attemptTopicSummarizationIfNeeded(); + sendHistoryRef.set(finalHistory); + notifySendState(); + if (renderTarget) renderTarget.renderMessage(assistantMessage); + if (!request?.conversation) await attemptTopicSummarizationIfNeeded(); } else { console.log(`[ChatManager] Saved non-streaming response for background chat: Agent ${responseContext.agentId}, Topic ${responseContext.topicId}`); } @@ -1814,35 +1959,42 @@ window.chatManager = (() => { } } else { await removeThinkingFromSource(); - if (isForActiveChat && messageRenderer) { - messageRenderer.renderMessage({ role: 'system', content: 'VCP 返回了未知格式的响应。', timestamp: Date.now() }); + if (isForActiveChat && renderTarget) { + renderTarget.renderMessage({ role: 'system', content: 'VCP 返回了未知格式的响应。', timestamp: Date.now() }); } } } else { - if (vcpResponse && vcpResponse.streamError) { + if (vcpResponse && vcpResponse.streamError) { console.error("Streaming setup failed in main process:", vcpResponse.errorDetail || vcpResponse.error); await removeThinkingFromSource(); - if (isSendContextCurrent() && messageRenderer) { - messageRenderer.renderMessage({ role: 'system', content: `请求流式回复失败: ${vcpResponse.error || '未知错误'}`, timestamp: Date.now() }); - } - } else if (vcpResponse && !vcpResponse.streamingStarted && !vcpResponse.streamError) { + if (isSendContextCurrent() && renderTarget) { + renderTarget.renderMessage({ role: 'system', content: `请求流式回复失败: ${vcpResponse.error || '未知错误'}`, timestamp: Date.now() }); + } + if (request?.propagateError) throw new Error(String(vcpResponse.error || '流式回复失败')); + } else if (vcpResponse && !vcpResponse.streamingStarted && !vcpResponse.streamError) { console.warn("Expected streaming to start, but main process returned non-streaming or error:", vcpResponse); await removeThinkingFromSource(); - if (isSendContextCurrent() && messageRenderer) { - messageRenderer.renderMessage({ role: 'system', content: '请求流式回复失败,收到非流式响应或错误。', timestamp: Date.now() }); - } - } - } - } catch (error) { + if (isSendContextCurrent() && renderTarget) { + renderTarget.renderMessage({ role: 'system', content: '请求流式回复失败,收到非流式响应或错误。', timestamp: Date.now() }); + } + if (request?.propagateError) throw new Error('请求流式回复失败,收到非流式响应或错误'); + } + if (request?.awaitTerminal && ownedStreamTerminal) { + const terminal = await ownedStreamTerminal; + return Object.freeze({ messageId: thinkingMessage.id, terminal }); + } + } + } catch (error) { console.error('发送消息或处理VCP响应时出错', error); await removeThinkingFromSource(); - if (isSendContextCurrent() && messageRenderer) { - messageRenderer.renderMessage({ role: 'system', content: `错误: ${error.message}`, timestamp: Date.now() }); - } - } - } finally { - pendingSendContexts.delete(sendSignature); - } + if (isSendContextCurrent() && renderTarget) { + renderTarget.renderMessage({ role: 'system', content: `错误: ${error.message}`, timestamp: Date.now() }); + } + if (request?.propagateError) throw error; + } + } finally { + pendingSendContexts.delete(sendSignature); + } } async function createNewTopicForItem(itemId, itemType) { @@ -1888,7 +2040,7 @@ window.chatManager = (() => { if (!isCreationCurrent() || watcherLease?.stale || watcherLease?.success === false) return; currentTopicIdRef.set(result.topicId); currentChatHistoryRef.set([]); - window.updateSendButtonState?.(); + notifySendStateChanged(); if (messageRenderer) { messageRenderer.setCurrentTopicId(result.topicId); @@ -1991,11 +2143,7 @@ window.chatManager = (() => { const newTopicId = createResult.topicId; - if (itemType === 'agent') { - saveResult = await electronAPI.saveChatHistory(itemId, newTopicId, historyForNewBranch); - } else { // group - saveResult = await electronAPI.saveGroupChatHistory(itemId, newTopicId, historyForNewBranch); - } + saveResult = await saveHistory(itemId, itemType, newTopicId, historyForNewBranch); if (!saveResult || !saveResult.success) { uiHelper.showToastNotification(`无法将历史记录保存到新的分支话题: ${saveResult ? saveResult.error : '未知错误'}`, 'error'); @@ -2045,7 +2193,9 @@ window.chatManager = (() => { await selectItem(target.id, target.type, target.name, targetItemFullConfig.avatarUrl, targetItemFullConfig); // 3. After a brief delay to allow the UI to update from selectItem, populate and send. - setTimeout(async () => { + const timer = setTimeout(async () => { + forwardTimers.delete(timer); + if (disposed) return; // 4. Populate the message input and attachments ref messageInput.value = content; @@ -2068,13 +2218,15 @@ window.chatManager = (() => { // 5. Call the standard send message handler to trigger the full AI response flow await handleSendMessage(); - }, 200); // 200ms delay seems reasonable for UI transition + }, 200); // 200ms delay seems reasonable for UI transition + forwardTimers.add(timer); } // --- Canvas Integration --- const CANVAS_PLACEHOLDER = '{{VCPChatCanvas}}'; - function handleCanvasContentUpdate(data) { + function handleCanvasContentUpdate(data) { + if (disposed) return; isCanvasWindowOpen = true; const { messageInput } = elements; // If the canvas is open and there's content, ensure the placeholder is in the input @@ -2086,7 +2238,8 @@ window.chatManager = (() => { } } - function handleCanvasWindowClosed() { + function handleCanvasWindowClosed() { + if (disposed) return; isCanvasWindowOpen = false; const { messageInput } = elements; // Remove the placeholder when the window is closed @@ -2098,8 +2251,22 @@ window.chatManager = (() => { } - async function syncHistoryFromFile(itemId, itemType, topicId) { - if (!messageRenderer) return; + async function syncHistoryFromFile(itemId, itemType, topicId) { + const syncGeneration = ++historySyncGeneration; + const itemGeneration = itemSelectionGeneration; + const topicGeneration = topicSelectionGeneration; + const isSyncCurrent = () => { + const selectedItem = currentSelectedItemRef.get(); + return !disposed + && Boolean(messageRenderer) + && syncGeneration === historySyncGeneration + && itemGeneration === itemSelectionGeneration + && topicGeneration === topicSelectionGeneration + && selectedItem?.id === itemId + && selectedItem?.type === itemType + && currentTopicIdRef.get() === topicId; + }; + if (!isSyncCurrent()) return; // 🔧 检查是否有正在进行的编辑操作 const isEditing = document.querySelector('.message-item-editing'); @@ -2108,15 +2275,24 @@ window.chatManager = (() => { return; } - // 1. Fetch the latest history from the file + // Capture active stream ownership before the async read. A stream may + // reach terminal while the read is in flight; that terminal must not + // be mistaken for a file-side deletion when the active id disappears. + const activeStreamingIdAtRead = streamProjection?.getActiveStreamingMessageId?.() || null; + + // 1. Fetch the latest history from the file let newHistory; if (itemType === 'agent') { - newHistory = await electronAPI.getChatHistory(itemId, topicId); - } else if (itemType === 'group') { - newHistory = await electronAPI.getGroupChatHistory(itemId, topicId); - } - - if (!newHistory || newHistory.error) { + newHistory = await getHistory(itemId, itemType, topicId); + } else if (itemType === 'group') { + newHistory = await getHistory(itemId, itemType, topicId); + } + + // A file notification may resolve after navigation, a newer sync, or + // teardown. Only the still-selected conversation may update its view. + if (!isSyncCurrent()) return; + + if (!newHistory || newHistory.error) { console.error("Sync failed: Could not fetch new history.", newHistory?.error); return; } @@ -2126,15 +2302,18 @@ window.chatManager = (() => { const oldHistoryMap = new Map(oldHistory.map(msg => [msg.id, msg])); const newHistoryMap = new Map(newHistory.map(msg => [msg.id, msg])); - const activeStreamingId = window.streamManager ? window.streamManager.getActiveStreamingMessageId() : null; + const activeStreamingId = streamProjection?.getActiveStreamingMessageId?.() || null; + const protectedStreamingIds = new Set( + [activeStreamingIdAtRead, activeStreamingId].filter(Boolean), + ); // --- Perform UI and Memory updates --- // 2. Handle DELETED and MODIFIED messages for (const oldMsg of oldHistory) { - if (oldMsg.id === activeStreamingId) { - continue; // Protect the currently streaming message - } + if (protectedStreamingIds.has(oldMsg.id)) { + continue; // Protect the currently streaming message + } const newMsgData = newHistoryMap.get(oldMsg.id); @@ -2173,8 +2352,8 @@ window.chatManager = (() => { // 4. If messages were added or removed, the order might be wrong. Re-sort. // Also ensures the streaming message (if any) is at the very end. historyInMem.sort((a, b) => { - if (a.id === activeStreamingId) return 1; - if (b.id === activeStreamingId) return -1; + if (protectedStreamingIds.has(a.id)) return 1; + if (protectedStreamingIds.has(b.id)) return -1; return a.timestamp - b.timestamp; }); @@ -2191,15 +2370,44 @@ window.chatManager = (() => { - // --- Public API --- - return { + async function dispose() { + if (disposed) return; + disposed = true; + initialized = false; + itemSelectionGeneration += 1; + topicSelectionGeneration += 1; + topicCreationGeneration += 1; + activeHistoryLoadToken += 1; + historySyncGeneration += 1; + pendingItemSelectionToken = null; + emptyStateObserver?.disconnect(); + emptyStateObserver = null; + canvasContentDisposer?.(); + canvasClosedDisposer?.(); + canvasContentDisposer = null; + canvasClosedDisposer = null; + for (const timer of forwardTimers) clearTimeout(timer); + forwardTimers.clear(); + await Promise.allSettled([ + lastOpenSaveQueue, + ...outgoingPersistenceQueues.values(), + ]); + outgoingPersistenceQueues.clear(); + pendingSendContexts.clear(); + } + + // --- Public API --- + return { init, + dispose, + isReady: () => initialized, selectItem, restoreLastOpenState, selectTopic, handleTopicDeletion, loadChatHistory, - handleSendMessage, + handleSendMessage, + sendMessage: handleSendMessage, createNewTopicForItem, displayNoItemSelected, syncNextUiEmptyStateWithMessages, @@ -2211,5 +2419,4 @@ window.chatManager = (() => { processFilesData, syncHistoryFromFile, // Expose the new function }; -})(); - +})(); diff --git a/modules/event-listeners.js b/modules/event-listeners.js index f44104967..d8415ef6f 100644 --- a/modules/event-listeners.js +++ b/modules/event-listeners.js @@ -29,11 +29,16 @@ export function setupEventListeners(deps) { refs, // Modules and helper functions - uiHelperFunctions, chatManager, itemListManager, settingsManager, uiManager, topicListManager, + uiHelperFunctions, chatManager, messageRenderer, historyMutationAuthority, itemListManager, settingsManager, uiManager, topicListManager, getCroppedFile, setCroppedFile, updateAttachmentPreview, filterAgentList, - addNetworkPathInput + addNetworkPathInput, sendButtonAction, listenerOwner } = deps; + const addListener = (target, type, handler, options) => listenerOwner?.add(target, type, handler, options) || target?.addEventListener?.(type, handler, options); + const setOwnedTimeout = (callback, delay) => listenerOwner?.timeout?.(callback, delay) ?? setTimeout(callback, delay); + const releaseCapturedListeners = listenerOwner?.capture?.() || (() => {}); + let setupCompleted = false; + try { const setupAutoHideScrollbar = (container, hideDelayMs = 700) => { if (!container) return; if (container.dataset.autoHideScrollbarBound === 'true') return; @@ -42,7 +47,7 @@ export function setupEventListeners(deps) { const showScrollingState = () => { container.classList.add('is-scrolling'); if (hideTimer) clearTimeout(hideTimer); - hideTimer = setTimeout(() => { + hideTimer = setOwnedTimeout(() => { container.classList.remove('is-scrolling'); hideTimer = null; }, hideDelayMs); @@ -227,10 +232,6 @@ export function setupEventListeners(deps) { avatarColor: (currentSelectedItem.config || currentSelectedItem)?.avatarCalculatedColor }; - let thinkingMessageItem = null; - if (window.messageRenderer) { - thinkingMessageItem = await window.messageRenderer.renderMessage(thinkingMessage); - } currentChatHistory.push(thinkingMessage); try { @@ -295,13 +296,6 @@ export function setupEventListeners(deps) { stream: useStreaming }; - if (useStreaming) { - if (window.messageRenderer) { - await new Promise(resolve => setTimeout(resolve, 500)); - await window.messageRenderer.startStreamingMessage({ ...thinkingMessage, content: "" }, thinkingMessageItem); - } - } - const context = { agentId: currentSelectedItem.id, agentName: currentSelectedItem.name || currentSelectedItem.id, @@ -326,12 +320,12 @@ export function setupEventListeners(deps) { const isForActiveChat = context && context.agentId === currentSelectedItem.id && context.topicId === currentTopicId; if (isForActiveChat) { - if (window.messageRenderer) window.messageRenderer.removeMessageById(thinkingMessage.id); + messageRenderer?.removeMessageById(thinkingMessage.id); } if (response.error) { - if (isForActiveChat && window.messageRenderer) { - window.messageRenderer.renderMessage({ role: 'system', content: `VCP错误: ${response.error}`, timestamp: Date.now() }); + if (isForActiveChat && messageRenderer) { + messageRenderer.renderMessage({ role: 'system', content: `VCP错误: ${response.error}`, timestamp: Date.now() }); } console.error(`[ContinueWriting] VCP Error:`, response.error); } else if (response.choices && response.choices.length > 0) { @@ -350,13 +344,16 @@ export function setupEventListeners(deps) { if (historyForSave && !historyForSave.error) { const finalHistory = historyForSave.filter(msg => msg.id !== thinkingMessage.id && !msg.isThinking); finalHistory.push(assistantMessage); - await chatAPI.saveChatHistory(context.agentId, context.topicId, finalHistory); + await historyMutationAuthority.replace({ + itemId: context.agentId, itemType: 'agent', topicId: context.topicId, + category: 'flowlock-non-stream-terminal', + }, finalHistory); if (isForActiveChat) { currentChatHistory.length = 0; currentChatHistory.push(...finalHistory); - if (window.messageRenderer) window.messageRenderer.renderMessage(assistantMessage); - await window.chatManager.attemptTopicSummarizationIfNeeded(); + messageRenderer?.renderMessage(assistantMessage); + await chatManager.attemptTopicSummarizationIfNeeded(); } } } @@ -368,10 +365,13 @@ export function setupEventListeners(deps) { } catch (error) { console.error('[ContinueWriting] 续写时出错:', error); - if (window.messageRenderer) window.messageRenderer.removeMessageById(thinkingMessage.id); - if (window.messageRenderer) window.messageRenderer.renderMessage({ role: 'system', content: `错误: ${error.message}`, timestamp: Date.now() }); + messageRenderer?.removeMessageById(thinkingMessage.id); + messageRenderer?.renderMessage({ role: 'system', content: `错误: ${error.message}`, timestamp: Date.now() }); if (currentSelectedItem.id && currentTopicId) { - await chatAPI.saveChatHistory(currentSelectedItem.id, currentTopicId, currentChatHistory.filter(msg => !msg.isThinking)); + await historyMutationAuthority.replace({ + itemId: currentSelectedItem.id, itemType: currentSelectedItem.type, topicId: currentTopicId, + category: 'flowlock-failure-cleanup', + }, currentChatHistory.filter(msg => !msg.isThinking)); } } } @@ -380,7 +380,7 @@ export function setupEventListeners(deps) { window.handleContinueWriting = handleContinueWriting; if (chatMessagesDiv) { - chatMessagesDiv.addEventListener('click', (event) => { + addListener(chatMessagesDiv, 'click', (event) => { // Stop TTS playback when clicking a speaking avatar const avatar = event.target.closest('.chat-avatar'); if (avatar && avatar.classList.contains('speaking')) { @@ -423,16 +423,16 @@ export function setupEventListeners(deps) { console.error('[Renderer] chatMessagesDiv not found during setupEventListeners.'); } - sendMessageBtn.addEventListener('click', async () => { - if (typeof window.handleSendButtonAction === 'function' && sendMessageBtn?.dataset.mode === 'interrupt') { - await window.handleSendButtonAction(); + addListener(sendMessageBtn, 'click', async () => { + if (typeof sendButtonAction === 'function') { + await sendButtonAction(); return; } chatManager.handleSendMessage(); }); // 发送按钮右键 - 打开「高级回复」(VCPChatTarven) 浮窗 - sendMessageBtn.addEventListener('contextmenu', (e) => { + addListener(sendMessageBtn, 'contextmenu', (e) => { e.preventDefault(); if (window.TavernManager && typeof window.TavernManager.togglePopover === 'function') { window.TavernManager.togglePopover(sendMessageBtn); @@ -440,15 +440,15 @@ export function setupEventListeners(deps) { console.warn('[EventListeners] TavernManager not available.'); } }); - messageInput.addEventListener('keydown', (e) => { + addListener(messageInput, 'keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatManager.handleSendMessage(); } }); - messageInput.addEventListener('input', () => uiHelperFunctions.autoResizeTextarea(messageInput)); + addListener(messageInput, 'input', () => uiHelperFunctions.autoResizeTextarea(messageInput)); - messageInput.addEventListener('mousedown', async (e) => { + addListener(messageInput, 'mousedown', async (e) => { if (e.button === 1) { // 中键 e.preventDefault(); e.stopPropagation(); @@ -471,7 +471,7 @@ export function setupEventListeners(deps) { } }); - attachFileBtn.addEventListener('click', async () => { + addListener(attachFileBtn, 'click', async () => { const currentSelectedItem = refs.currentSelectedItem.get(); const currentTopicId = refs.currentTopicId.get(); if (!currentSelectedItem.id || !currentTopicId) { @@ -486,7 +486,7 @@ export function setupEventListeners(deps) { console.error(`Error processing selected file ${att.name || 'unknown'}: ${att.error}`); uiHelperFunctions.showToastNotification(`处理文件 ${att.name || '未知文件'} 失败: ${att.error}`, 'error'); } else { - refs.attachedFiles.get().push({ + refs.attachedFiles.append({ file: { name: att.name, type: att.type, size: att.size }, localPath: att.internalPath, originalName: att.name, @@ -622,7 +622,7 @@ export function setupEventListeners(deps) { currentActive.classList.remove('active'); // 等待退出动画完成后显示新面板 - setTimeout(() => { + setOwnedTimeout(() => { currentActive.style.display = 'none'; currentActive.classList.remove('switching-out'); @@ -1066,8 +1066,8 @@ export function setupEventListeners(deps) { document.body.appendChild(menu); - setTimeout(() => { - document.addEventListener('click', closeMenu, true); + setOwnedTimeout(() => { + addListener(document, 'click', closeMenu, true); }, 0); } @@ -1249,7 +1249,10 @@ export function setupEventListeners(deps) { }); if (window.filterManager?.subscribe) { - window.filterManager.subscribe(syncNotificationFilterState); + // State-channel subscriptions are lifecycle resources too. Keep the + // disposer with the main renderer owner so a remount/reload cannot + // retain a callback closed over stale settings refs. + listenerOwner?.own(window.filterManager.subscribe(syncNotificationFilterState)); } else { window.addEventListener('notification-filter-changed', event => syncNotificationFilterState(event.detail)); syncNotificationFilterState(); @@ -1385,7 +1388,7 @@ export function setupEventListeners(deps) { } }); - chatAPI.onDoToggleNotificationsSidebar(() => { + listenerOwner?.own(chatAPI.onDoToggleNotificationsSidebar(() => { const isActive = notificationsSidebar.classList.toggle('active'); const mainContent = document.querySelector('.main-content'); if (mainContent) { @@ -1395,7 +1398,7 @@ export function setupEventListeners(deps) { notificationsSidebar.style.width = `${refs.globalSettings.get().notificationsSidebarWidth}px`; } syncNotificationTogglePlacement(isActive); - }); + })); syncNotificationTogglePlacement(); } @@ -1406,10 +1409,10 @@ export function setupEventListeners(deps) { let sidebarLongPressTimer = null; let wasSidebarLongPress = false; - const saveSidebarState = () => { + const saveSidebarState = settings => { if (!chatAPI?.saveSettings) return; - chatAPI.saveSettings(refs.globalSettings.get()).then(result => { + chatAPI.saveSettings(settings || refs.globalSettings.get()).then(result => { if (!result.success) { console.error('保存侧边栏状态失败:', result.error); } @@ -1440,9 +1443,13 @@ export function setupEventListeners(deps) { } const globalSettings = refs.globalSettings.get(); - globalSettings.sidebarAvatarOnly = enabled; - if (enabled) globalSettings.sidebarActive = true; - saveSidebarState(); + const nextSettings = { + ...globalSettings, + sidebarAvatarOnly: enabled, + sidebarActive: enabled ? true : globalSettings.sidebarActive, + }; + refs.globalSettings.set(nextSettings); + saveSidebarState(nextSettings); return true; }; @@ -1457,10 +1464,13 @@ export function setupEventListeners(deps) { const isActive = target.classList.toggle('active'); syncSidebarVisibility(isActive); - const globalSettings = refs.globalSettings.get(); - globalSettings.sidebarActive = isActive; - globalSettings.sidebarAvatarOnly = false; - saveSidebarState(); + const nextSettings = { + ...refs.globalSettings.get(), + sidebarActive: isActive, + sidebarAvatarOnly: false, + }; + refs.globalSettings.set(nextSettings); + saveSidebarState(nextSettings); uiHelperFunctions.showToastNotification(`侧栏已${isActive ? '显示' : '隐藏'}`, 'info'); }; @@ -1470,7 +1480,7 @@ export function setupEventListeners(deps) { toggleAssistantBtn.addEventListener('mousedown', (e) => { if (e.button === 0) { wasLongPress = false; - longPressTimer = setTimeout(() => { + longPressTimer = setOwnedTimeout(() => { console.log('[Assistant] Long press detected on toggle button'); chatAPI.assistantAction('open'); wasLongPress = true; @@ -1507,18 +1517,18 @@ export function setupEventListeners(deps) { const globalSettings = refs.globalSettings.get(); const isActive = toggleAssistantBtn.classList.toggle('active'); - globalSettings.assistantEnabled = isActive; + const nextSettings = { ...globalSettings, assistantEnabled: isActive }; + refs.globalSettings.set(nextSettings); chatAPI.toggleSelectionListener(isActive); const result = await chatAPI.saveSettings({ - ...globalSettings, - assistantEnabled: isActive + ...nextSettings }); if (result.success) { uiHelperFunctions.showToastNotification(`划词助手已${isActive ? '开启' : '关闭'}`, 'info'); } else { uiHelperFunctions.showToastNotification(`设置划词助手状态失败: ${result.error}`, 'error'); toggleAssistantBtn.classList.toggle('active', !isActive); - globalSettings.assistantEnabled = !isActive; + refs.globalSettings.set({ ...globalSettings, assistantEnabled: !isActive }); } }); @@ -1526,7 +1536,7 @@ export function setupEventListeners(deps) { toggleSidebarModeBtn.addEventListener('mousedown', e => { if (e.button !== 0) return; wasSidebarLongPress = false; - sidebarLongPressTimer = setTimeout(() => { + sidebarLongPressTimer = setOwnedTimeout(() => { sidebarLongPressTimer = null; if (!leftSidebar) return; @@ -1667,7 +1677,7 @@ export function setupEventListeners(deps) { // 监听来自主进程的全局快捷键触发的创建未锁定话题事件 if (chatAPI?.onCreateUnlockedTopic) { - chatAPI.onCreateUnlockedTopic(() => { + listenerOwner?.own(chatAPI.onCreateUnlockedTopic(() => { console.log('[快捷键] 收到来自主进程的创建未锁定话题请求'); const currentSelectedItem = refs.currentSelectedItem.get(); if (!currentSelectedItem.id) { @@ -1679,7 +1689,12 @@ export function setupEventListeners(deps) { return; } createNewTopicWithLockStatus(currentSelectedItem, false); - }); + })); } + setupCompleted = true; + } finally { + releaseCapturedListeners(); + if (!setupCompleted) eventListenersBound = false; + } } diff --git a/modules/global-settings-manager.js b/modules/global-settings-manager.js index 6a159abac..834fc43c8 100644 --- a/modules/global-settings-manager.js +++ b/modules/global-settings-manager.js @@ -33,11 +33,20 @@ async function saveGlobalSettings(deps, settingsForm) { const { refs, + messageRenderer, getCroppedFile, setCroppedFile, uiHelperFunctions, - settingsManager + settingsManager, + normalizeChatPresentationMode, + applyChatPresentationMode, + applyChatBubbleLayoutSettings, + getAppearance = () => window.VCPAppearance } = deps; + if (typeof normalizeChatPresentationMode !== 'function' || typeof applyChatPresentationMode !== 'function' + || typeof applyChatBubbleLayoutSettings !== 'function') { + throw new TypeError('Global settings save requires presentation and layout capabilities.'); + } const currentSettings = refs.globalSettings.get(); const clampBubbleWidthPercent = (rawValue, fallback) => { @@ -87,7 +96,7 @@ async function saveGlobalSettings(deps, settingsForm) { showHomeVisualTagline: document.getElementById('showHomeVisualTagline')?.checked !== false, homeVisualTagline: document.getElementById('homeVisualTagline')?.value.trim().slice(0, 120) || '语义级打穿 AI、UI/UX、APP 与人类想象力的边界', - appearanceProfile: window.VCPAppearance?.normalize({ + appearanceProfile: getAppearance()?.normalize({ density: document.getElementById('appearanceDensity')?.value, radius: document.getElementById('appearanceRadius')?.value, typography: document.getElementById('appearanceTypography')?.value, @@ -125,10 +134,10 @@ async function saveGlobalSettings(deps, settingsForm) { chatToolFontPreset: document.getElementById('chatToolFontPreset')?.value || currentSettings.chatToolFontPreset || 'system', chatToolFontCustom: document.getElementById('chatToolFontCustom')?.value.trim() || '', enableWideChatLayout: document.getElementById('chatLayoutModeWide')?.checked || false, - chatPresentationMode: window.normalizeChatPresentationMode?.( + chatPresentationMode: normalizeChatPresentationMode( document.querySelector('input[name="chatPresentationMode"]:checked')?.value || currentSettings.chatPresentationMode - ) || 'bubble', + ), enableUserChatBubbleUi: document.getElementById('enableUserChatBubbleUi')?.checked !== false, showUserMetaInChatBubbleUi: document.getElementById('showUserMetaInChatBubbleUi')?.checked !== false, chatBubbleMaxWidthDefault: clampBubbleWidthPercent(currentSettings.chatBubbleMaxWidthDefault, 82), @@ -208,7 +217,7 @@ async function saveGlobalSettings(deps, settingsForm) { buffer: arrayBuffer }); if (avatarSaveResult.success) { - refs.globalSettings.get().userAvatarUrl = avatarSaveResult.avatarUrl; + newSettings.userAvatarUrl = avatarSaveResult.avatarUrl; const userAvatarPreview = document.getElementById('userAvatarPreview'); userAvatarPreview.src = avatarSaveResult.avatarUrl; userAvatarPreview.style.display = 'block'; @@ -219,9 +228,7 @@ async function saveGlobalSettings(deps, settingsForm) { userAvatarWrapper.classList.remove('no-avatar'); } - if (window.messageRenderer) { - window.messageRenderer.setUserAvatar(avatarSaveResult.avatarUrl); - } + messageRenderer?.setUserAvatar(avatarSaveResult.avatarUrl); if (avatarSaveResult.needsColorExtraction && chatAPI?.saveAvatarColor) { if (window.getDominantAvatarColor) { window.getDominantAvatarColor(avatarSaveResult.avatarUrl).then(avgColor => { @@ -229,8 +236,9 @@ async function saveGlobalSettings(deps, settingsForm) { chatAPI.saveAvatarColor({ type: 'user', id: 'user_global', color: avgColor }) .then((saveColorResult) => { if (saveColorResult && saveColorResult.success) { - refs.globalSettings.get().userAvatarCalculatedColor = avgColor; - if (window.messageRenderer) window.messageRenderer.setUserAvatarColor(avgColor); + const current = refs.globalSettings.get(); + refs.globalSettings.set?.({ ...current, userAvatarCalculatedColor: avgColor }); + messageRenderer?.setUserAvatarColor(avgColor); } else { console.warn("Failed to save user avatar color:", saveColorResult?.error); } @@ -286,20 +294,21 @@ async function saveGlobalSettings(deps, settingsForm) { } } - Object.assign(refs.globalSettings.get(), newSettings); try { - newSettings.appearanceProfile = window.VCPAppearance?.commit( + newSettings.appearanceProfile = getAppearance()?.commit( newSettings.appearanceProfile, { uiMode: 'next', source: 'settings-save' } ) || newSettings.appearanceProfile; + const committedSettings = { ...currentSettings, ...newSettings }; + refs.globalSettings.set?.(committedSettings); window.dispatchEvent(new CustomEvent('global-settings-updated', { - detail: { settings: refs.globalSettings.get(), source: 'settings-save' } + detail: { settings: committedSettings, source: 'settings-save' } })); - if (typeof window.applyChatBubbleLayoutSettings === 'function') { - window.applyChatBubbleLayoutSettings(refs.globalSettings.get()); + if (typeof applyChatBubbleLayoutSettings === 'function') { + applyChatBubbleLayoutSettings(committedSettings); } - if (typeof window.applyChatPresentationMode === 'function') { - await window.applyChatPresentationMode(newSettings.chatPresentationMode, { + if (typeof applyChatPresentationMode === 'function') { + await applyChatPresentationMode(newSettings.chatPresentationMode, { persist: false, preserveScroll: true, source: 'global-settings' diff --git a/modules/inputEnhancer.js b/modules/inputEnhancer.js index 8039b19c1..8b7140883 100644 --- a/modules/inputEnhancer.js +++ b/modules/inputEnhancer.js @@ -7,8 +7,9 @@ const LONG_TEXT_THRESHOLD = 2000; // Characters let attachedFilesRef; // Reference to renderer.js's attachedFiles array let updateAttachmentPreviewRef; // Reference to renderer.js's updateAttachmentPreview function let currentAgentIdRef; // Function to get currentAgentId -let currentTopicIdRef; // Function to get currentTopicId -let electronApiRef; // Stable API reference for helper functions outside initializeInputEnhancer +let currentTopicIdRef; // Function to get currentTopicId +let electronApiRef; // Stable API reference for helper functions outside initializeInputEnhancer +let inputEnhancerDispose = async () => {}; /** * Initializes the input enhancer module. @@ -19,9 +20,10 @@ let electronApiRef; // Stable API reference for helper functions outside initial * @param {Array} refs.attachedFiles - Reference to the array holding files to be attached. * @param {Function} refs.updateAttachmentPreview - Function to update the attachment preview UI. * @param {Function} refs.getCurrentAgentId - Function that returns the current agent ID. - * @param {Function} refs.getCurrentTopicId - Function that returns the current topic ID. - */ -function initializeInputEnhancer(refs) { + * @param {Function} refs.getCurrentTopicId - Function that returns the current topic ID. + * @param {object} [refs.listenerOwner] - Renderer lifecycle owner for DOM listeners. + */ +function initializeInputEnhancer(refs) { if (!refs.messageInput || !refs.electronAPI || !refs.attachedFiles || !refs.updateAttachmentPreview || !refs.getCurrentAgentId || !refs.getCurrentTopicId) { console.error('[InputEnhancer] Initialization failed: Missing required references.'); return; @@ -34,7 +36,39 @@ function initializeInputEnhancer(refs) { currentTopicIdRef = refs.getCurrentTopicId; electronApiRef = localElectronAPI; - const messageInput = refs.messageInput; + void inputEnhancerDispose(); + const ownedDisposers = []; + const lifecycle = { active: true, tasks: new Set() }; + const isActive = () => lifecycle.active; + const track = value => { + const task = Promise.resolve(value); + lifecycle.tasks.add(task); + task.finally(() => lifecycle.tasks.delete(task)).catch(() => {}); + return task; + }; + const addListener = (target, type, handler, options) => { + const ownedHandler = (...args) => { + if (!isActive()) return; + try { + const result = handler(...args); + return result?.then ? track(result) : result; + } catch (error) { + console.error(`[InputEnhancer] ${type} listener failed:`, error); + } + }; + if (refs.listenerOwner?.add?.(target, type, ownedHandler, options)) return; + target?.addEventListener?.(type, ownedHandler, options); + ownedDisposers.push(() => target?.removeEventListener?.(type, ownedHandler, options)); + }; + inputEnhancerDispose = async () => { + if (!lifecycle.active) return; + lifecycle.active = false; + ownedDisposers.splice(0).reverse().forEach(dispose => dispose()); + noteSuggestionPopup?.remove?.(); + noteSuggestionPopup = null; + await Promise.allSettled([...lifecycle.tasks]); + }; + const messageInput = refs.messageInput; const dropTargetElement = refs.dropTargetElement || messageInput; let dragDepth = 0; @@ -45,7 +79,7 @@ function initializeInputEnhancer(refs) { }; // 1. Drag and Drop functionality - dropTargetElement.addEventListener('dragenter', (event) => { + addListener(dropTargetElement, 'dragenter', (event) => { event.preventDefault(); event.stopPropagation(); console.log('[InputEnhancer] dragenter event'); @@ -53,7 +87,7 @@ function initializeInputEnhancer(refs) { activateDropTarget(); }); - dropTargetElement.addEventListener('dragover', (event) => { + addListener(dropTargetElement, 'dragover', (event) => { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'copy'; @@ -62,7 +96,7 @@ function initializeInputEnhancer(refs) { } }); - dropTargetElement.addEventListener('dragleave', (event) => { + addListener(dropTargetElement, 'dragleave', (event) => { event.preventDefault(); event.stopPropagation(); console.log('[InputEnhancer] dragleave event'); @@ -72,7 +106,7 @@ function initializeInputEnhancer(refs) { } }); - dropTargetElement.addEventListener('drop', async (event) => { + addListener(dropTargetElement, 'drop', async (event) => { event.preventDefault(); event.stopPropagation(); console.log('[InputEnhancer] drop event triggered.'); @@ -132,7 +166,8 @@ function initializeInputEnhancer(refs) { })); } - const droppedFilesData = await Promise.all(filesToProcess); + const droppedFilesData = await Promise.all(filesToProcess); + if (!isActive()) return; const successfulFiles = droppedFilesData.filter(f => !f.error); const failedFiles = droppedFilesData.filter(f => f.error); @@ -151,20 +186,19 @@ function initializeInputEnhancer(refs) { try { console.log('[InputEnhancer] Calling localElectronAPI.handleFileDrop with:', agentId, topicId, successfulFiles.map(f => ({ name: f.name, type: f.type, size: f.size, data: f.data ? `[Buffer, length: ${f.data.length}]` : 'N/A' }))); // Pass the actual buffers to main process - const results = await localElectronAPI.handleFileDrop(agentId, topicId, successfulFiles); + const results = await localElectronAPI.handleFileDrop(agentId, topicId, successfulFiles); + if (!isActive()) return; console.log('[InputEnhancer] Results from handleFileDrop:', results); if (results && results.length > 0) { results.forEach(result => { if (result.success && result.attachment) { const att = result.attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); console.log(`[InputEnhancer] Successfully attached dropped file: ${att.name}`); } else if (result.error) { console.error(`[InputEnhancer] Error processing dropped file ${result.name || 'unknown'}: ${result.error}`); @@ -183,7 +217,7 @@ function initializeInputEnhancer(refs) { }); // 2. Enhanced Paste functionality - messageInput.addEventListener('paste', async (event) => { + addListener(messageInput, 'paste', async (event) => { const agentId = currentAgentIdRef(); const topicId = currentTopicIdRef(); const clipboardData = event.clipboardData || window.clipboardData; @@ -214,15 +248,16 @@ function initializeInputEnhancer(refs) { if (items[i].kind === 'file') { const file = items[i].getAsFile(); if (file) { - await handlePastedFileSafe(file, agentId, topicId); + await handlePastedFileSafe(file, agentId, topicId, lifecycle, localElectronAPI); return; // 婢跺嫮鎮婄€瑰瞼顑囨稉鈧稉顏呮瀮娴犺泛姘ㄧ紒鎾存将 } } } // 婵″倹鐏夐張澶嬪焻閸ユ拝绱濇稊鐔虹暬閺傚洣娆㈤敍灞肩瑐闂堛垻娈戦柅鏄忕帆娴兼艾顦╅悶? - const imageData = await localElectronAPI.readImageFromClipboard(); - if (imageData && imageData.data) { - await handlePastedImageDataSafe(imageData, agentId, topicId); + const imageData = await localElectronAPI.readImageFromClipboard(); + if (!isActive()) return; + if (imageData && imageData.data) { + await handlePastedImageDataSafe(imageData, agentId, topicId, lifecycle, localElectronAPI); return; } @@ -236,7 +271,7 @@ function initializeInputEnhancer(refs) { alert('请先选择一个 Agent 和话题,再粘贴长文本。'); return; } - await handleLongTextPasteSafe(pastedText, agentId, topicId); + await handleLongTextPasteSafe(pastedText, agentId, topicId, lifecycle, localElectronAPI); return; } @@ -247,7 +282,7 @@ function initializeInputEnhancer(refs) { console.log('[InputEnhancer] Event listeners attached to message input.'); // Listen for files shared from other windows (like the music player) - localElectronAPI.onAddFileToInput(async (filePath) => { + const consumeSharedFile = async (filePath) => { console.log(`[InputEnhancer] Received shared file path: ${filePath}`); const agentId = currentAgentIdRef(); const topicId = currentTopicIdRef(); @@ -261,35 +296,41 @@ function initializeInputEnhancer(refs) { // The main process will read the file content from the path. try { // We need to get the filename from the path to mimic a real File object. - const fileName = await window.electronPath.basename(filePath); - const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ path: filePath, name: fileName }]); + const fileName = await window.electronPath.basename(filePath); + if (!isActive()) return; + const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ path: filePath, name: fileName }]); + if (!isActive()) return; if (results && results.length > 0 && results[0].success && results[0].attachment) { const att = results[0].attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); updateAttachmentPreviewRef(); console.log(`[InputEnhancer] Successfully attached shared file: ${att.name}`); } else { const errorMsg = results && results.length > 0 && results[0].error ? results[0].error : '閺堫亞鐓¢柨娆掝嚖'; alert('附加分享的文件失败: ' + errorMsg); } - } catch (err) { - console.error('[InputEnhancer] Error attaching shared file:', err); - alert('附加分享的文件时发生意外错误。'); - } - }); + } catch (err) { + if (!isActive()) return; + console.error('[InputEnhancer] Error attaching shared file:', err); + alert('附加分享的文件时发生意外错误。'); + } + }; + const sharedFileSubscription = localElectronAPI.onAddFileToInput(filePath => { + if (!isActive()) return; + return track(consumeSharedFile(filePath)); + }); + refs.listenerOwner?.own?.(sharedFileSubscription); // --- @note Mention Functionality --- let noteSuggestionPopup = null; let activeSuggestionIndex = -1; - messageInput.addEventListener('input', async () => { + addListener(messageInput, 'input', async () => { const text = messageInput.value; const cursorPos = messageInput.selectionStart; const atMatch = text.substring(0, cursorPos).match(/@([\w\u4e00-\u9fa5]*)$/); @@ -298,7 +339,8 @@ function initializeInputEnhancer(refs) { const query = atMatch[1]; try { console.log('[inputEnhancer] Searching notes for query:', query); - const notes = await localElectronAPI.searchNotes(query); + const notes = await localElectronAPI.searchNotes(query); + if (!isActive()) return; console.log('[inputEnhancer] Search results:', notes); if (notes && notes.length > 0) { @@ -306,8 +348,9 @@ function initializeInputEnhancer(refs) { } else { hideNoteSuggestions(); } - } catch (error) { - console.error('[inputEnhancer] Failed to search notes:', error); + } catch (error) { + if (!isActive()) return; + console.error('[inputEnhancer] Failed to search notes:', error); hideNoteSuggestions(); } } else { @@ -315,7 +358,7 @@ function initializeInputEnhancer(refs) { } }); - messageInput.addEventListener('keydown', (e) => { + addListener(messageInput, 'keydown', (e) => { if (noteSuggestionPopup && noteSuggestionPopup.style.display === 'block') { const items = noteSuggestionPopup.querySelectorAll('.suggestion-item'); if (e.key === 'ArrowDown') { @@ -358,7 +401,7 @@ function initializeInputEnhancer(refs) { `; item.dataset.filePath = note.path; - item.addEventListener('click', () => selectNoteSuggestion(note)); + addListener(item, 'click', () => selectNoteSuggestion(note)); noteSuggestionPopup.appendChild(item); }); @@ -407,30 +450,30 @@ function initializeInputEnhancer(refs) { // Attach the file using existing logic try { - const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ + const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ path: note.path, // Pass the full path name: note.name // No need to specify type or data, main process will handle it - }]); + }]); + if (!isActive()) return; if (results && results.length > 0 && results[0].success && results[0].attachment) { const att = results[0].attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); updateAttachmentPreviewRef(); console.log(`[InputEnhancer] Successfully attached note: ${att.name}`); } else { const errorMsg = results && results.length > 0 && results[0].error ? results[0].error : '閺堫亞鐓¢柨娆掝嚖'; alert('附加笔记 "' + note.name + '" 失败: ' + errorMsg); } - } catch (err) { - console.error('[InputEnhancer] Error attaching note file:', err); + } catch (err) { + if (!isActive()) return; + console.error('[InputEnhancer] Error attaching note file:', err); alert('附加笔记 "' + note.name + '" 时发生意外错误。'); } } @@ -463,107 +506,105 @@ function readFileAsUint8Array(file) { }); } -async function handlePastedFileSafe(file, agentId, topicId) { - try { - const localElectronAPI = getElectronApi(); - const fileBuffer = await readFileAsUint8Array(file); +async function handlePastedFileSafe(file, agentId, topicId, lifecycle, localElectronAPI) { + try { + const fileBuffer = await readFileAsUint8Array(file); + if (!lifecycle.active) return; if (fileBuffer.length === 0 && file.size !== 0) { alert('无法读取粘贴文件 "' + file.name + '" 的内容。'); return; } - const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ + const results = await localElectronAPI.handleFileDrop(agentId, topicId, [{ name: file.name, type: file.type || 'application/octet-stream', data: fileBuffer, size: file.size - }]); + }]); + if (!lifecycle.active) return; if (results && results.length > 0 && results[0].success && results[0].attachment) { const att = results[0].attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); updateAttachmentPreviewRef(); return; } const errorMsg = results && results.length > 0 && results[0].error ? results[0].error : '未知错误'; alert('粘贴文件 "' + file.name + '" 失败: ' + errorMsg); - } catch (error) { + } catch (error) { + if (!lifecycle.active) return; console.error('[InputEnhancer] Failed to handle pasted file:', error); alert('粘贴文件 "' + file.name + '" 时发生错误: ' + error.message); } } -async function handlePastedImageDataSafe(imageData, agentId, topicId) { - try { - const localElectronAPI = getElectronApi(); - const result = await localElectronAPI.handleFilePaste(agentId, topicId, { +async function handlePastedImageDataSafe(imageData, agentId, topicId, lifecycle, localElectronAPI) { + try { + const result = await localElectronAPI.handleFilePaste(agentId, topicId, { type: 'base64', data: imageData.data, extension: imageData.extension || 'png' - }); + }); + if (!lifecycle.active) return; if (result.success && result.attachment) { const att = result.attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); updateAttachmentPreviewRef(); } else { alert('无法从剪贴板粘贴图片: ' + (result.error || '截图处理失败')); } - } catch (error) { + } catch (error) { + if (!lifecycle.active) return; console.error('[InputEnhancer] Failed to handle pasted image data:', error); alert('粘贴截图时发生错误: ' + error.message); } } -async function handleLongTextPasteSafe(pastedText, agentId, topicId) { - try { - const localElectronAPI = getElectronApi(); - const result = await localElectronAPI.handleTextPasteAsFile(agentId, topicId, pastedText); +async function handleLongTextPasteSafe(pastedText, agentId, topicId, lifecycle, localElectronAPI) { + try { + const result = await localElectronAPI.handleTextPasteAsFile(agentId, topicId, pastedText); + if (!lifecycle.active) return; if (result.success && result.attachment) { const att = result.attachment; - const currentFiles = attachedFilesRef.get(); - currentFiles.push({ - file: { name: att.name, type: att.type, size: att.size }, - localPath: att.internalPath, - originalName: att.name, - _fileManagerData: att - }); - attachedFilesRef.set(currentFiles); + attachedFilesRef.append({ + file: { name: att.name, type: att.type, size: att.size }, + localPath: att.internalPath, + originalName: att.name, + _fileManagerData: att + }); updateAttachmentPreviewRef(); } else { alert('长文本转存为 .txt 文件失败: ' + (result.error || '未知错误')); } - } catch (error) { + } catch (error) { + if (!lifecycle.active) return; console.error('[InputEnhancer] Failed to handle long text paste:', error); alert('长文本转存时发生错误: ' + error.message); } } -async function handlePastedFile(file, agentId, topicId) { - return handlePastedFileSafe(file, agentId, topicId); +async function handlePastedFile(file, agentId, topicId) { + return handlePastedFileSafe(file, agentId, topicId, { active: true }, getElectronApi()); } -async function handlePastedImageData(imageData, agentId, topicId) { - return handlePastedImageDataSafe(imageData, agentId, topicId); +async function handlePastedImageData(imageData, agentId, topicId) { + return handlePastedImageDataSafe(imageData, agentId, topicId, { active: true }, getElectronApi()); } -async function handleLongTextPaste(pastedText, agentId, topicId) { - return handleLongTextPasteSafe(pastedText, agentId, topicId); +async function handleLongTextPaste(pastedText, agentId, topicId) { + return handleLongTextPasteSafe(pastedText, agentId, topicId, { active: true }, getElectronApi()); } @@ -584,6 +625,7 @@ function insertTextAtCursor(inputElement, text) { inputElement.dispatchEvent(new Event('input', { bubbles: true })); } -window.inputEnhancer = { - initializeInputEnhancer -}; +window.inputEnhancer = { + initializeInputEnhancer, + dispose: () => inputEnhancerDispose() +}; diff --git a/modules/ipc/assistantHandlers.js b/modules/ipc/assistantHandlers.js index 6fcc57843..dbbd2794c 100644 --- a/modules/ipc/assistantHandlers.js +++ b/modules/ipc/assistantHandlers.js @@ -1111,12 +1111,25 @@ function createAssistantWindow(data) { show: false, resizable: true, alwaysOnTop: false, - }); - assistantWindow.loadFile(path.join(__dirname, '..', '..', 'rust_assistant_engine', 'ui', 'assistant.html')); - assistantWindow.once('ready-to-show', () => { - assistantWindow.show(); - assistantWindow.webContents.send('assistant-data', data); - }); + }); + // Deliver the producer payload after the renderer has installed its + // preload/DOM listener. `ready-to-show` is a presentation event and can + // race the page's `onAssistantData` registration when another auxiliary + // window is opening at the same time. + assistantWindow.webContents.once('did-finish-load', () => { + if (!assistantWindow || assistantWindow.isDestroyed()) return; + assistantWindow.webContents.send('assistant-data', data); + }); + assistantWindow.loadFile(path.join(__dirname, '..', '..', 'rust_assistant_engine', 'ui', 'assistant.html')); + assistantWindow.webContents.on('render-process-gone', (_event, details) => { + console.warn('[Assistant] assistant renderer exited; releasing window owner:', details?.reason || 'unknown'); + const crashedWindow = assistantWindow; + assistantWindow = null; + if (crashedWindow && !crashedWindow.isDestroyed()) crashedWindow.destroy(); + }); + assistantWindow.once('ready-to-show', () => { + assistantWindow.show(); + }); assistantWindow.on('closed', () => { assistantWindow = null; }); diff --git a/modules/ipc/chatHandlers.js b/modules/ipc/chatHandlers.js index 96811c241..5683def47 100644 --- a/modules/ipc/chatHandlers.js +++ b/modules/ipc/chatHandlers.js @@ -861,6 +861,7 @@ function initialize(mainWindow, context) { ipcMain.handle('send-to-vcp', async (event, vcpUrl, vcpApiKey, messages, modelConfig, messageId, isGroupCall = false, context = null) => { console.log(`[Main - sendToVCP] ***** sendToVCP HANDLER EXECUTED for messageId: ${messageId}, isGroupCall: ${isGroupCall} *****`, context); const streamChannel = 'vcp-stream-event'; // Use a single, unified channel for all stream events. + const streamOperationId = `${messageId}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`; let streamTask = null; let streamTaskDetached = false; @@ -872,7 +873,7 @@ function initialize(mainWindow, context) { const sendStreamPayload = payload => { if (streamTask?.controller.signal.aborted || event.sender.isDestroyed()) return false; try { - event.sender.send(streamChannel, payload); + event.sender.send(streamChannel, { ...payload, streamOperationId }); return true; } catch (error) { console.warn(`[Main - sendToVCP] Dropped stream event for ${messageId}:`, error.message); diff --git a/modules/ipc/voiceHandlers.js b/modules/ipc/voiceHandlers.js index c35fa69e2..eb6d079c2 100644 --- a/modules/ipc/voiceHandlers.js +++ b/modules/ipc/voiceHandlers.js @@ -42,6 +42,13 @@ function createVoiceChatWindow(agentId) { openChildWindows.push(voiceChatWindow); + voiceChatWindow.webContents.on('render-process-gone', (_event, details) => { + console.warn('[VoiceHandlers] voice renderer exited; releasing window owner:', details?.reason || 'unknown'); + const index = openChildWindows.indexOf(voiceChatWindow); + if (index > -1) openChildWindows.splice(index, 1); + if (!voiceChatWindow.isDestroyed()) voiceChatWindow.destroy(); + }); + voiceChatWindow.on('closed', () => { const index = openChildWindows.indexOf(voiceChatWindow); if (index > -1) { @@ -107,4 +114,4 @@ function initialize(options) { module.exports = { initialize, createVoiceChatWindow -}; \ No newline at end of file +}; diff --git a/modules/mainChatCommands.js b/modules/mainChatCommands.js index f1aa89f96..5850f33b5 100644 --- a/modules/mainChatCommands.js +++ b/modules/mainChatCommands.js @@ -1,5 +1,8 @@ (() => { + let chatManagerProvider = null; + let capabilities = Object.create(null); const api = () => window.chatAPI || window.electronAPI; + const capability = (name, fallback) => capabilities[name] || fallback?.(); const windowCommand = (name, fallback) => window.VCPWindowState?.[name] ? window.VCPWindowState[name]() : fallback?.(); @@ -20,7 +23,7 @@ } function openSettings() { - const open = () => window.uiHelperFunctions?.openModal?.('globalSettingsModal'); + const open = () => capability('uiHelper', () => window.uiHelperFunctions)?.openModal?.('globalSettingsModal'); return window.VCPPerformance?.measure ? window.VCPPerformance.measure('settings.open', open, { source: 'main-chat-command' }) : open(); @@ -31,19 +34,19 @@ } function toggleTheme() { - const currentTheme = window.uiManager?.getThemeState?.()?.effective || 'light'; + const currentTheme = capability('uiManager', () => window.uiManager)?.getThemeState?.()?.effective || 'light'; const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; - if (!window.VCPAppearanceStudio?.setThemeMode?.(nextTheme, { source: 'main-chat-command' })) { + if (!capability('appearanceStudio', () => window.VCPAppearanceStudio)?.setThemeMode?.(nextTheme, { source: 'main-chat-command' })) { api()?.setTheme?.(nextTheme); } } function createItem() { - return window.topTabManager?.openCreateDialog?.(); + return capability('topTabManager', () => window.topTabManager)?.openCreateDialog?.(); } function notify(message, type = 'error') { - window.uiHelperFunctions?.showToastNotification?.(message, type); + capability('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.(message, type); } async function openForum() { @@ -75,15 +78,15 @@ } function toggleNotificationFilter() { - return window.filterManager?.toggleFilterMode?.(); + return capability('filterManager', () => window.filterManager)?.toggleFilterMode?.(); } function openNotificationFilterSettings() { - return window.filterManager?.openFilterRulesModal?.(); + return capability('filterManager', () => window.filterManager)?.openFilterRulesModal?.(); } function clearNotifications() { - return window.notificationRenderer?.clearPersistentNotifications?.() + return capability('notificationRenderer', () => window.notificationRenderer)?.clearPersistentNotifications?.() || { success: false, removed: 0 }; } @@ -91,16 +94,24 @@ return Boolean(signal?.aborted); } + function chatManagerReady() { + return Boolean(chatManagerProvider?.selectItem) + && (typeof chatManagerProvider.isReady !== 'function' || chatManagerProvider.isReady()); + } + async function createAgent({ name, model = '', signal = null }) { const result = await api()?.createAgent?.(name, model ? { model } : undefined); if (!result?.success) return result || { success: false, error: '创建功能不可用' }; if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; + if (!chatManagerReady()) { + return { ...result, navigationSuccess: false, warning: '聊天界面尚未就绪,请稍后重试。' }; + } try { - await window.itemListManager?.loadItems?.(); + await capability('itemListManager', () => window.itemListManager)?.loadItems?.(); if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; - await window.chatManager?.selectItem?.(result.agentId, 'agent', result.agentName, null, result.config); + await chatManagerProvider?.selectItem?.(result.agentId, 'agent', result.agentName, null, result.config); if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; - window.uiManager?.switchToTab?.('settings'); + capability('uiManager', () => window.uiManager)?.switchToTab?.('settings'); return { ...result, navigationSuccess: true }; } catch (error) { console.error('[MainChatCommands] Agent created but UI navigation failed:', error); @@ -116,12 +127,15 @@ const group = result.agentGroup; if (!group?.id) return { success: false, error: '群组已创建,但返回数据不完整。' }; if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; + if (!chatManagerReady()) { + return { ...result, navigationSuccess: false, warning: '聊天界面尚未就绪,请稍后重试。' }; + } try { - await window.itemListManager?.loadItems?.(); + await capability('itemListManager', () => window.itemListManager)?.loadItems?.(); if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; - await window.chatManager?.selectItem?.(group.id, 'group', group.name, group.avatarUrl, group); + await chatManagerProvider?.selectItem?.(group.id, 'group', group.name, group.avatarUrl, group); if (isAborted(signal)) return { ...result, navigationSuccess: false, cancelled: true }; - window.uiManager?.switchToTab?.('settings'); + capability('uiManager', () => window.uiManager)?.switchToTab?.('settings'); return { ...result, navigationSuccess: true }; } catch (error) { console.error('[MainChatCommands] Group created but UI navigation failed:', error); @@ -158,6 +172,21 @@ name, `main.${name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`, ])); + window.addEventListener('vcp-main-chat-commands-configure', event => { + const provider = event.detail?.chatManager; + const nextCapabilities = event.detail?.capabilities; + if (!provider || typeof provider.selectItem !== 'function') { + throw new TypeError('MainChatCommands requires a chat manager provider.'); + } + if (!nextCapabilities || typeof nextCapabilities !== 'object') { + throw new TypeError('MainChatCommands capabilities must be an object.'); + } + if (chatManagerProvider && chatManagerProvider !== provider) { + throw new Error('MainChatCommands chat manager provider is already registered.'); + } + chatManagerProvider = provider; + capabilities = Object.freeze({ ...nextCapabilities }); + }, { once: true }); if (commandRegistry) { Object.entries(handlers).forEach(([name, handler]) => { const id = commandIds[name]; @@ -168,12 +197,12 @@ name, (...args) => commandRegistry ? commandRegistry.execute(commandIds[name], ...args) : handler(...args), ])); - window.MainChatCommands = Object.freeze({ + Object.defineProperty(window, 'MainChatCommands', { value: Object.freeze({ ...facade, getWindowState: () => window.VCPWindowState?.getState?.() || Object.freeze({ ready: false, maximized: false }), subscribeWindowState: (listener, options) => window.VCPWindowState?.subscribe?.(listener, options) || (() => false), execute: (id, ...args) => commandRegistry?.execute(id, ...args), list: () => commandRegistry?.list() || [], register: (definition, options) => commandRegistry?.register(definition, options), - }); + }), writable: false, configurable: false }); })(); diff --git a/modules/messageRenderer.js b/modules/messageRenderer.js index 4528592f9..4ca43559d 100644 --- a/modules/messageRenderer.js +++ b/modules/messageRenderer.js @@ -1,40 +1,16 @@ -// modules/messageRenderer.js - -// --- Enhanced Rendering Constants --- -const ENHANCED_RENDER_DEBOUNCE_DELAY = 400; // ms, for general blocks during streaming -const DIARY_RENDER_DEBOUNCE_DELAY = 1000; // ms, potentially longer for diary if complex -const enhancedRenderDebounceTimers = new WeakMap(); // For debouncing prettify calls - -// 🟢 大内容截断阈值与缓存 -const TOOL_RESULT_TRUNCATE_THRESHOLD = 50000; // 50KB 以上触发截断 -const TOOL_RESULT_TRUNCATE_LINES = 80; // 截断后只显示前80行 -const toolResultFullContentMap = new Map(); // placeholderId -> { raw: string, fieldKey: string } -let toolResultContentIdCounter = 0; - -// 🟢 完整 Markdown → HTML 渲染缓存:只缓存 raw HTML 字符串,不缓存 DOM / 后处理结果 / message 对象。 -const RENDER_PIPELINE_VERSION = '2026-07-26-dollar-guard-v3'; -const RENDER_HTML_CACHE_MAX_BYTES = 20 * 1024 * 1024; -const RENDER_HTML_CACHE_MAX_ENTRIES = 500; -const RENDER_HTML_CACHE_MAX_SINGLE_BYTES = 1024 * 1024; -const RENDER_HTML_CACHE_MIN_TEXT_LENGTH = 512; -const RENDER_HTML_CACHE_MAX_TEXT_LENGTH = 512 * 1024; -const renderHtmlCache = new Map(); -let renderHtmlCacheBytes = 0; -const renderHtmlCacheStats = { - hits: 0, - misses: 0, - skips: 0, - evictions: 0 -}; - -import { avatarColorCache, getDominantAvatarColor } from './renderer/colorUtils.js'; -import { initializeImageHandler, setContentAndProcessImages } from './renderer/imageHandler.js'; -import { processAnimationsInContent, cleanupAnimationsInContent } from './renderer/animation.js'; -import * as visibilityOptimizer from './renderer/visibilityOptimizer.js'; -import { createMessageSkeleton, formatMessageTimestamp } from './renderer/domBuilder.js'; -import * as streamManager from './renderer/streamManager.js'; -import * as emoticonUrlFixer from './renderer/emoticonUrlFixer.js'; -import { createContentPipeline, PIPELINE_MODES } from './renderer/contentPipeline.js'; +import { avatarColorCache, getDominantAvatarColor } from './renderer/colorUtils.js'; +import { createImageHandler } from './renderer/imageHandler.js'; +import { processAnimationsInContent, cleanupAnimationsInContent } from './renderer/animation.js'; +import { createVisibilityOptimizer } from './renderer/visibilityOptimizer.js'; +import { createMessageSkeleton, formatMessageTimestamp } from './renderer/domBuilder.js'; +import { createEmoticonUrlFixer } from './renderer/emoticonUrlFixer.js'; +import { createContentPipeline, PIPELINE_MODES } from './renderer/contentPipeline.js'; +import { createContentRuntime } from './chat/contentRuntime.js'; +import { createMermaidPlaceholderTransform } from './chat/contentTransforms.js'; +import { createChatDomRenderer } from './chat/chatDomRenderer.js'; +import { createRenderDependencies } from './renderer/renderDependencies.js'; +import { createRenderSessionAuthority } from './renderer/renderSessionAuthority.js'; +import { createSurfaceTaskOwner } from './renderer/surfaceTaskOwner.js'; import { TOOL_REQUEST_START_MARKER as TOOL_START_MARKER, TOOL_REQUEST_END_MARKER as TOOL_END_MARKER, @@ -43,20 +19,66 @@ import { replaceToolRequestBlocks } from './renderer/toolRequestScanner.js'; -const colorExtractionPromises = new Map(); - -async function getDominantAvatarColorCached(url) { - if (!colorExtractionPromises.has(url)) { - colorExtractionPromises.set(url, getDominantAvatarColor(url)); - } - return colorExtractionPromises.get(url); -} - -import * as contentProcessor from './renderer/contentProcessor.js'; -import * as contextMenu from './renderer/messageContextMenu.js'; - - -import * as middleClickHandler from './renderer/middleClickHandler.js'; +import { createContentProcessor } from './renderer/contentProcessor.js'; +import { createMessageContextMenu } from './renderer/messageContextMenu.js'; + + +import { createMiddleClickHandler } from './renderer/middleClickHandler.js'; + +// modules/messageRenderer.js + +export function createMessageRenderer(options = {}) { +const surfaceId = String(options.surfaceId || `surface-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`); +const features = Object.freeze({ + contextMenu: options.enableContextMenu !== false, + middleClick: options.enableMiddleClick !== false, + streamProjection: options.initializeStreamProjection !== false, + globalCommands: options.exposeGlobalCommands !== false, +}); +const visibilityOptimizer = options.visibilityOptimizer || createVisibilityOptimizer(); +const streamManager = options.streamManager; +if (!streamManager) throw new TypeError('MessageRenderer requires an owned StreamProjection'); +const emoticonUrlFixer = options.emoticonUrlFixer || createEmoticonUrlFixer(); +const contentProcessor = options.contentProcessor || createContentProcessor(); +const contextMenu = options.contextMenu || createMessageContextMenu(); +const middleClickHandler = options.middleClickHandler || createMiddleClickHandler(); +const imageHandler = options.imageHandler || createImageHandler({ fixUrl: emoticonUrlFixer.fixEmoticonUrl }); +const colorExtractionPromises = new Map(); +const ownedStyleElements = new Set(); + +async function getDominantAvatarColorCached(url) { + if (!colorExtractionPromises.has(url)) { + colorExtractionPromises.set(url, getDominantAvatarColor(url)); + } + return colorExtractionPromises.get(url); +} + +// --- Enhanced Rendering Constants --- +const ENHANCED_RENDER_DEBOUNCE_DELAY = 400; // ms, for general blocks during streaming +const DIARY_RENDER_DEBOUNCE_DELAY = 1000; // ms, potentially longer for diary if complex +const enhancedRenderDebounceTimers = new WeakMap(); // For debouncing prettify calls + +// 🟢 大内容截断阈值与缓存 +const TOOL_RESULT_TRUNCATE_THRESHOLD = 50000; // 50KB 以上触发截断 +const TOOL_RESULT_TRUNCATE_LINES = 80; // 截断后只显示前80行 +const toolResultFullContentMap = new Map(); // placeholderId -> { raw: string, fieldKey: string } +let toolResultContentIdCounter = 0; + +// 🟢 完整 Markdown → HTML 渲染缓存:只缓存 raw HTML 字符串,不缓存 DOM / 后处理结果 / message 对象。 +const RENDER_PIPELINE_VERSION = '2026-07-26-dollar-guard-v3'; +const RENDER_HTML_CACHE_MAX_BYTES = 20 * 1024 * 1024; +const RENDER_HTML_CACHE_MAX_ENTRIES = 500; +const RENDER_HTML_CACHE_MAX_SINGLE_BYTES = 1024 * 1024; +const RENDER_HTML_CACHE_MIN_TEXT_LENGTH = 512; +const RENDER_HTML_CACHE_MAX_TEXT_LENGTH = 512 * 1024; +const renderHtmlCache = new Map(); +let renderHtmlCacheBytes = 0; +const renderHtmlCacheStats = { + hits: 0, + misses: 0, + skips: 0, + evictions: 0 +}; // --- LaTeX Protection --- @@ -535,7 +557,7 @@ async function renderMermaidDiagrams(container) { } } -function enhanceMermaidDiagram(mermaidElement) { +function enhanceMermaidDiagram(mermaidElement) { if (!mermaidElement || mermaidElement.dataset.vcpMermaidEnhanced === 'true') return; const svg = mermaidElement.querySelector('svg'); @@ -543,13 +565,21 @@ function enhanceMermaidDiagram(mermaidElement) { mermaidElement.dataset.vcpMermaidEnhanced = 'true'; - const wrapper = document.createElement('div'); + const ownerDocument = mermaidElement.ownerDocument; + if (!ownerDocument) return; + const wrapper = ownerDocument.createElement('div'); wrapper.className = 'mermaid-viewer'; wrapper.dataset.scale = '1'; wrapper.dataset.translateX = '0'; - wrapper.dataset.translateY = '0'; - - const toolbar = document.createElement('div'); + wrapper.dataset.translateY = '0'; + const disposers = []; + const listen = (target, type, handler, options) => { + target.addEventListener(type, handler, options); + disposers.push(() => target.removeEventListener(type, handler, options)); + }; + wrapper._vcpDisposers = disposers; + + const toolbar = ownerDocument.createElement('div'); toolbar.className = 'mermaid-viewer-toolbar'; toolbar.innerHTML = ` @@ -558,11 +588,11 @@ function enhanceMermaidDiagram(mermaidElement) { `; - const viewport = document.createElement('div'); + const viewport = ownerDocument.createElement('div'); viewport.className = 'mermaid-viewer-viewport'; viewport.title = '滚轮缩放,按住鼠标左键拖拽平移,双击重置'; - const canvas = document.createElement('div'); + const canvas = ownerDocument.createElement('div'); canvas.className = 'mermaid-viewer-canvas'; svg.removeAttribute('style'); @@ -602,7 +632,7 @@ function enhanceMermaidDiagram(mermaidElement) { }); }; const resetView = () => setState({ scale: 1, translateX: 0, translateY: 0 }); - const fitToWidth = () => { + const fitToWidth = () => { const svgWidth = svg.getBBox?.().width || svg.viewBox?.baseVal?.width || svg.getBoundingClientRect().width; const availableWidth = Math.max(1, viewport.clientWidth - 32); if (!svgWidth) { @@ -613,10 +643,11 @@ function enhanceMermaidDiagram(mermaidElement) { scale: clampScale(Math.min(1.8, availableWidth / svgWidth)), translateX: 0, translateY: 0 - }); - }; + }); + }; + - toolbar.addEventListener('click', (event) => { + listen(toolbar, 'click', (event) => { const button = event.target.closest('[data-mermaid-action]'); if (!button) return; @@ -629,35 +660,36 @@ function enhanceMermaidDiagram(mermaidElement) { else if (action === 'zoom-out') zoomAt(scale / 1.2); else if (action === 'reset') resetView(); else if (action === 'fit') fitToWidth(); - }); - - viewport.addEventListener('wheel', (event) => { + }); + + listen(viewport, 'wheel', (event) => { event.preventDefault(); event.stopPropagation(); const rect = viewport.getBoundingClientRect(); const factor = event.deltaY < 0 ? 1.12 : 1 / 1.12; zoomAt(getState().scale * factor, event.clientX - rect.left, event.clientY - rect.top); - }, { passive: false }); + }, { passive: false }); let dragState = null; - viewport.addEventListener('pointerdown', (event) => { + listen(viewport, 'pointerdown', (event) => { if (event.button !== 0) return; const state = getState(); - dragState = { + dragState = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, originX: state.translateX, - originY: state.translateY - }; + originY: state.translateY + }; + wrapper._vcpPointerId = event.pointerId; viewport.classList.add('dragging'); viewport.setPointerCapture?.(event.pointerId); event.preventDefault(); }); - viewport.addEventListener('pointermove', (event) => { + listen(viewport, 'pointermove', (event) => { if (!dragState || dragState.pointerId !== event.pointerId) return; setState({ @@ -668,33 +700,60 @@ function enhanceMermaidDiagram(mermaidElement) { }); const endDrag = (event) => { - if (!dragState || dragState.pointerId !== event.pointerId) return; - dragState = null; + if (!dragState || dragState.pointerId !== event.pointerId) return; + dragState = null; + delete wrapper._vcpPointerId; viewport.classList.remove('dragging'); viewport.releasePointerCapture?.(event.pointerId); }; - viewport.addEventListener('pointerup', endDrag); - viewport.addEventListener('pointercancel', endDrag); - viewport.addEventListener('dblclick', (event) => { + listen(viewport, 'pointerup', endDrag); + listen(viewport, 'pointercancel', endDrag); + listen(viewport, 'dblclick', (event) => { event.preventDefault(); resetView(); }); - requestAnimationFrame(fitToWidth); -} + const ownerWindow = ownerDocument.defaultView; + const requestFrame = ownerWindow?.requestAnimationFrame?.bind(ownerWindow) || ((callback) => ownerWindow?.setTimeout?.(() => callback(Date.now()), 0)); + wrapper._vcpFitRaf = requestFrame?.(() => { + delete wrapper._vcpFitRaf; + if (wrapper.isConnected) fitToWidth(); + }); +} + +function cleanupMermaidViewers(contentDiv) { + contentDiv?.querySelectorAll?.('.mermaid-viewer').forEach(wrapper => { + wrapper._vcpDisposers?.splice(0).forEach(dispose => { + try { dispose(); } catch { /* listener already removed */ } + }); + if (wrapper._vcpFitRaf) { + const ownerWindow = wrapper.ownerDocument?.defaultView; + if (typeof ownerWindow?.cancelAnimationFrame === 'function') ownerWindow.cancelAnimationFrame(wrapper._vcpFitRaf); + else ownerWindow?.clearTimeout?.(wrapper._vcpFitRaf); + delete wrapper._vcpFitRaf; + } + const viewport = wrapper.querySelector('.mermaid-viewer-viewport'); + const pointerId = wrapper._vcpPointerId; + if (pointerId !== undefined && viewport?.hasPointerCapture?.(pointerId)) { + try { viewport.releasePointerCapture(pointerId); } catch { /* pointer already released */ } + } + wrapper.replaceChildren(); + delete wrapper._vcpPointerId; + }); +} function getCompiledRegex(rule) { if (!rule?.findPattern) { return null; } - if (window.uiHelperFunctions?.getCompiledRegex) { - const compiled = window.uiHelperFunctions.getCompiledRegex(rule.findPattern); + if (mainRendererReferences?.uiHelper?.getCompiledRegex) { + const compiled = mainRendererReferences.uiHelper.getCompiledRegex(rule.findPattern); return compiled?.regex || null; } - if (window.uiHelperFunctions?.regexFromString) { - return window.uiHelperFunctions.regexFromString(rule.findPattern); + if (mainRendererReferences?.uiHelper?.regexFromString) { + return mainRendererReferences.uiHelper.regexFromString(rule.findPattern); } const regexMatch = rule.findPattern.match(/^\/(.+?)\/([gimuy]*)$/); @@ -1293,15 +1352,21 @@ function processAndInjectScopedCss(content, scopeId) { if (cssContent.length > 0) { try { const scopedCss = contentProcessor.scopeCss(cssContent, scopeId); - const styleSelector = `style[data-vcp-scope-id="${escapeCssAttributeValue(scopeId)}"]`; - let styleElement = document.head.querySelector(styleSelector); - - if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.setAttribute('data-vcp-scope-id', scopeId); - document.head.appendChild(styleElement); - } + // Scope IDs are generated per message, but include the surface namespace + // in the lookup as a hard boundary when two renderers share a document. + const styleSelector = `style[data-vcp-scope-id="${escapeCssAttributeValue(scopeId)}"][data-vcp-surface-id="${escapeCssAttributeValue(surfaceId)}"]`; + const ownerDocument = mainRendererReferences?.document || mainRendererReferences?.chatMessagesDiv?.ownerDocument; + if (!ownerDocument?.head) throw new Error('MessageRenderer has no owning document head'); + let styleElement = [...ownedStyleElements].find((element) => element.matches?.(styleSelector)); + + if (!styleElement) { + styleElement = ownerDocument.createElement('style'); + styleElement.type = 'text/css'; + styleElement.setAttribute('data-vcp-scope-id', scopeId); + styleElement.setAttribute('data-vcp-surface-id', surfaceId); + ownerDocument.head.appendChild(styleElement); + ownedStyleElements.add(styleElement); + } // 流式渲染会多次经过此函数:复用节点并原子替换文本, // 避免同一消息累积多个样式节点或出现旧规则覆盖新规则。 @@ -1554,7 +1619,7 @@ function preprocessFullContent(text, settings = {}, messageRole = 'assistant', d return { text, toolResultMap: null }; } - const result = contentPipeline.process(text, { + const result = contentRuntime.processFull(text, { mode: PIPELINE_MODES.FULL_RENDER, settings, messageRole, @@ -1570,9 +1635,7 @@ function preprocessStreamTailContent(text) { return text; } - return contentPipeline.process(text, { - mode: PIPELINE_MODES.STREAM_FAST - }).text; + return contentRuntime.processStream(text).text; } function estimateStringBytes(str) { @@ -2179,115 +2242,163 @@ function fixEmoticonUrlsInMarkdown(text) { */ -let mainRendererReferences = { - currentChatHistoryRef: { get: () => [], set: () => { } }, // Ref to array - currentSelectedItemRef: { get: () => ({ id: null, type: null, name: null, avatarUrl: null, config: null }), set: () => { } }, // Ref to object - currentTopicIdRef: { get: () => null, set: () => { } }, // Ref to string/null - globalSettingsRef: { get: () => ({ userName: '用户', userAvatarUrl: 'assets/default_user_avatar.png', userAvatarCalculatedColor: null }), set: () => { } }, // Ref to object - - chatMessagesDiv: null, - electronAPI: null, - markedInstance: null, - uiHelper: { - scrollToBottom: () => { }, - openModal: () => { }, - autoResizeTextarea: () => { }, - // ... other uiHelper functions ... - }, - summarizeTopicFromMessages: async () => "", - handleCreateBranch: () => { }, - // activeStreamingMessageId: null, // ID of the message currently being streamed - REMOVED -}; - - -let contentPipeline = null; - -let activeRenderSessionId = 0; - -function invalidateRenderSession() { - activeRenderSessionId += 1; - return activeRenderSessionId; -} - -function getActiveRenderSessionId() { - return activeRenderSessionId; -} - -function isRenderSessionActive(sessionId) { - return sessionId === activeRenderSessionId; -} - -function escapeCssAttributeValue(value) { - const str = String(value); - if (window.CSS && typeof window.CSS.escape === 'function') { - return window.CSS.escape(str); +let mainRendererReferences = null; + + +let contentPipeline = null; +let contentRuntime = null; +let rendererListenerDisposers = []; +function disposeRendererListeners() { + rendererListenerDisposers.splice(0).reverse().forEach(dispose => { try { dispose(); } catch (error) { console.warn('[MessageRenderer] listener dispose failed:', error); } }); +} +function disposeRendererResources() { + disposeRendererListeners(); + contentProcessor.dispose?.(); + middleClickHandler.dispose?.(); + contextMenu.dispose?.(); + imageHandler.dispose?.(); + visibilityOptimizer.destroyVisibilityOptimizer?.(); + for (const styleElement of ownedStyleElements) { + styleElement.remove(); + } + ownedStyleElements.clear(); +} +async function disposeRootResources(root) { + if (!root?.querySelectorAll) return; + invalidateRenderSession(root); + await renderTaskOwner.dispose(root); + root.querySelectorAll('.message-item').forEach(item => cleanupMessageDomResources(item, item.dataset?.messageId || null)); + root.replaceChildren(); +} +function ownRendererListener(target, type, handler, options) { + target.addEventListener(type, handler, options); + rendererListenerDisposers.push(() => target.removeEventListener(type, handler, options)); +} + +const renderSessionAuthority = createRenderSessionAuthority({ + resolveDefaultRoot: () => mainRendererReferences?.chatMessagesDiv || null, +}); +const renderTaskOwner = createSurfaceTaskOwner(); + +function invalidateRenderSession(root = null) { + const ownerRoot = root || mainRendererReferences?.chatMessagesDiv || null; + if (ownerRoot) renderTaskOwner.revoke(ownerRoot); + return renderSessionAuthority.invalidate(ownerRoot); +} + +function getActiveRenderSessionId(root = null) { + return renderSessionAuthority.capture(root); +} + +function isRenderSessionActive(session) { + return renderSessionAuthority.isActive(session); +} + +function escapeCssAttributeValue(value) { + const str = String(value); + const ownerWindow = mainRendererReferences?.window || mainRendererReferences?.chatMessagesDiv?.ownerDocument?.defaultView; + if (ownerWindow?.CSS && typeof ownerWindow.CSS.escape === 'function') { + return ownerWindow.CSS.escape(str); } return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } -function cleanupScopedStylesForMessage(messageItem, messageId = null) { - if (!messageItem && !messageId) return; - - const scopeId = messageItem?.id; - if (scopeId) { - document.querySelectorAll(`style[data-vcp-scope-id="${escapeCssAttributeValue(scopeId)}"]`).forEach(el => el.remove()); - } - - const chatScopeId = messageItem?.getAttribute?.('data-chat-scope') || (messageId ? `vcp-chat-${messageId}` : null); - if (chatScopeId) { - document.querySelectorAll(`style[data-chat-scope-id="${escapeCssAttributeValue(chatScopeId)}"]`).forEach(el => el.remove()); - } -} - -function cleanupMessageDomResources(messageItem, messageId = null) { +function cleanupScopedStylesForMessage(messageItem, messageId = null) { + if (!messageItem && !messageId) return; + const scopeId = messageItem?.id; + const chatScopeId = messageItem?.getAttribute?.('data-chat-scope') || (messageId ? `vcp-${surfaceId}-chat-${messageId}` : null); + for (const styleElement of ownedStyleElements) { + if ((styleElement.getAttribute('data-vcp-surface-id') === surfaceId) + && ((scopeId && styleElement.getAttribute('data-vcp-scope-id') === scopeId) + || (chatScopeId && styleElement.getAttribute('data-chat-scope-id') === chatScopeId))) { + styleElement.remove(); + ownedStyleElements.delete(styleElement); + } + } +} + +function cleanupMessageDomResources(messageItem, messageId = null) { if (!messageItem) return; - const contentDiv = messageItem.querySelector('.md-content'); - if (contentDiv) { - contentProcessor.cleanupPreviewsInContent(contentDiv); - cleanupAnimationsInContent(contentDiv); + const contentDiv = messageItem.querySelector('.md-content'); + if (contentDiv) { + imageHandler.cleanupContent?.(contentDiv); + if (contentDiv._vcpDeferredHighlightTimer) { + clearTimeout(contentDiv._vcpDeferredHighlightTimer); + delete contentDiv._vcpDeferredHighlightTimer; + } + if (contentDiv._vcpPretextIdleHandle) { + const { kind, id } = contentDiv._vcpPretextIdleHandle; + const ownerWindow = contentDiv.ownerDocument?.defaultView; + if (kind === 'idle' && typeof ownerWindow?.cancelIdleCallback === 'function') { + ownerWindow.cancelIdleCallback(id); + } else if (kind === 'timer') { + clearTimeout(id); + } + delete contentDiv._vcpPretextIdleHandle; + } + cleanupMermaidViewers(contentDiv); + contentProcessor.cleanupPreviewsInContent(contentDiv); + cleanupAnimationsInContent(contentDiv); + contentDiv.querySelectorAll('[data-vcp-attachment-cleanup], .message-attachment-wrapper *').forEach(node => { + node._vcpAttachmentCleanup?.(); + delete node._vcpAttachmentCleanup; + }); + contentDiv.querySelectorAll('.vcp-audio-player').forEach(player => player._vcpAudioCleanup?.()); + contentDiv.querySelectorAll('video, audio').forEach(media => { + if (media.closest('.vcp-audio-player')) return; + try { media.pause?.(); } catch { /* detached media may already be closed */ } + media.removeAttribute('src'); + media.querySelectorAll?.('source').forEach(source => source.removeAttribute('src')); + try { media.load?.(); } catch { /* detached media may already be closed */ } + }); } - cleanupScopedStylesForMessage(messageItem, messageId || messageItem.dataset?.messageId || null); - visibilityOptimizer.unobserveMessage(messageItem); + cleanupScopedStylesForMessage(messageItem, messageId || messageItem.dataset?.messageId || null); + const ownedMessageId = messageId || messageItem.dataset?.messageId || null; + if (ownedMessageId && mainRendererReferences?.pretextBridge?.evict) { + mainRendererReferences.pretextBridge.evict(ownedMessageId); + } + visibilityOptimizer.unobserveMessage(messageItem); } -function removeMessageById(messageId, saveHistory = false) { - const item = mainRendererReferences.chatMessagesDiv.querySelector(`.message-item[data-message-id="${messageId}"]`); +function removeMessageById(messageId, saveHistory = false, root = mainRendererReferences.chatMessagesDiv) { + const item = root?.querySelector?.(`.message-item[data-message-id="${messageId}"]`); if (item) { // --- NEW: Cleanup dynamic content before removing from DOM --- cleanupMessageDomResources(item, messageId); // [Pretext集成] 释放高度缓存,防止内存泄漏 - if (window.pretextBridge && window.pretextBridge.evict) { - window.pretextBridge.evict(messageId); + if (mainRendererReferences.pretextBridge?.evict) { + mainRendererReferences.pretextBridge.evict(messageId); } item.remove(); } - const currentChatHistoryArray = mainRendererReferences.currentChatHistoryRef.get(); + const currentChatHistoryArray = [...mainRendererReferences.currentChatHistoryRef.get()]; const index = currentChatHistoryArray.findIndex(m => m.id === messageId); if (index > -1) { currentChatHistoryArray.splice(index, 1); - mainRendererReferences.currentChatHistoryRef.set([...currentChatHistoryArray]); - window.updateSendButtonState?.(); + mainRendererReferences.currentChatHistoryRef.set(currentChatHistoryArray); + mainRendererReferences.messageCommands.updateSendButtonState?.(); if (saveHistory) { const currentSelectedItemVal = mainRendererReferences.currentSelectedItemRef.get(); const currentTopicIdVal = mainRendererReferences.currentTopicIdRef.get(); if (currentSelectedItemVal.id && currentTopicIdVal) { - if (currentSelectedItemVal.type === 'agent') { - mainRendererReferences.electronAPI.saveChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); - } else if (currentSelectedItemVal.type === 'group' && mainRendererReferences.electronAPI.saveGroupChatHistory) { - mainRendererReferences.electronAPI.saveGroupChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); - } + void mainRendererReferences.historyMutationAuthority?.replace({ + itemId: currentSelectedItemVal.id, + itemType: currentSelectedItemVal.type, + topicId: currentTopicIdVal, + category: 'message-remove', + }, currentChatHistoryArray).catch(error => console.error('[MessageRenderer] history mutation failed:', error)); } } } } -function clearChat() { - invalidateRenderSession(); +function clearChat(root = mainRendererReferences.chatMessagesDiv) { + invalidateRenderSession(root); // 清空聊天通常意味着用户希望释放当前渲染上下文占用;HTML 字符串缓存不持有 DOM,但这里主动释放更保守。 clearRenderHtmlCache(); @@ -2297,31 +2408,23 @@ function clearChat() { toolResultFullContentMap.clear(); toolResultContentIdCounter = 0; - if (mainRendererReferences.chatMessagesDiv) { + if (root) { // --- NEW: Cleanup all messages before clearing the container --- - const allMessages = mainRendererReferences.chatMessagesDiv.querySelectorAll('.message-item'); + const allMessages = root.querySelectorAll('.message-item'); allMessages.forEach(item => { cleanupMessageDomResources(item, item.dataset?.messageId || null); }); - // 🟢 清理所有注入的 scoped CSS - document.querySelectorAll('style[data-vcp-scope-id]').forEach(el => el.remove()); - document.querySelectorAll('style[data-chat-scope-id]').forEach(el => el.remove()); - - // [Pretext集成] 清空所有高度缓存 - if (window.pretextBridge && window.pretextBridge.clearAll) { - window.pretextBridge.clearAll(); - } - - mainRendererReferences.chatMessagesDiv.innerHTML = ''; + root.innerHTML = ''; } mainRendererReferences.currentChatHistoryRef.set([]); // Clear the history array via its ref - window.updateSendButtonState?.(); + mainRendererReferences.messageCommands.updateSendButtonState?.(); } -function initializeMessageRenderer(refs) { - Object.assign(mainRendererReferences, refs); +function initializeMessageRenderer(refs) { + disposeRendererListeners(); + mainRendererReferences = createRenderDependencies(refs); contentPipeline = createContentPipeline({ escapeHtml, @@ -2335,38 +2438,34 @@ function initializeMessageRenderer(refs) { transformSpecialBlocks(text, codeBlockMap, thoughtChainMap), ensureHtmlFenced, transformFlowlockBlocks: (text) => { - if (!window.flowlockProtocol || typeof window.flowlockProtocol.transformForRender !== 'function') { - return text; - } - return window.flowlockProtocol.transformForRender(text); - }, - transformMermaidPlaceholders: (text) => { - let transformed = text.replace(MERMAID_CODE_REGEX, (match, lang, code) => { - const tempEl = document.createElement('textarea'); - tempEl.innerHTML = code; - const encodedCode = encodeURIComponent(tempEl.value.trim()); - return `
`; - }); - - transformed = transformed.replace(MERMAID_FENCE_REGEX, (match, lang, code) => { - const encodedCode = encodeURIComponent(code.trim()); - return `
`; - }); - - return transformed; + if (!mainRendererReferences.flowlockProtocol || typeof mainRendererReferences.flowlockProtocol.transformForRender !== 'function') { + return text; + } + return mainRendererReferences.flowlockProtocol.transformForRender(text); }, + transformMermaidPlaceholders: createMermaidPlaceholderTransform({ escapeHtml }), getToolResultRegex: () => TOOL_RESULT_REGEX, getToolRequestRegex: () => TOOL_REGEX, replaceToolRequestBlocks, getCodeFenceRegex: () => CODE_FENCE_REGEX, getDesktopPushRegex: () => DESKTOP_PUSH_REGEX, - getDesktopPushPartialRegex: () => DESKTOP_PUSH_PARTIAL_REGEX, - }); - - initializeImageHandler({ + getDesktopPushPartialRegex: () => DESKTOP_PUSH_PARTIAL_REGEX, + }); + contentRuntime = typeof createContentRuntime === 'function' + ? createContentRuntime({ pipeline: contentPipeline }) + : { + normalizeMessage: (message) => ({ ...(message || {}), id: message?.id || `msg_${Date.now()}`, role: message?.role || 'assistant', content: message?.content ?? '' }), + processFull: (text, options = {}) => contentPipeline.process(text, { ...options, mode: PIPELINE_MODES.FULL_RENDER }), + processStream: (text, options = {}) => contentPipeline.process(text, { ...options, mode: PIPELINE_MODES.STREAM_FAST }) + }; + + imageHandler.initialize({ electronAPI: mainRendererReferences.electronAPI, uiHelper: mainRendererReferences.uiHelper, - chatMessagesDiv: mainRendererReferences.chatMessagesDiv, + chatMessagesDiv: mainRendererReferences.chatMessagesDiv, + document: mainRendererReferences.document || mainRendererReferences.chatMessagesDiv?.ownerDocument, + window: mainRendererReferences.window || mainRendererReferences.chatMessagesDiv?.ownerDocument?.defaultView, + hljs: mainRendererReferences.hljs, }); // Start the emoticon fixer initialization, but don't wait for it here. @@ -2379,7 +2478,7 @@ function initializeMessageRenderer(refs) { visibilityOptimizer.initializeVisibilityOptimizer(scrollContainer || mainRendererReferences.chatMessagesDiv); // --- Event Delegation --- - mainRendererReferences.chatMessagesDiv.addEventListener('click', (e) => { + ownRendererListener(mainRendererReferences.chatMessagesDiv, 'click', (e) => { // 1. Handle collapsible tool results and thought chains const toolHeader = e.target.closest('.vcp-tool-result-header'); if (toolHeader) { @@ -2433,8 +2532,8 @@ function initializeMessageRenderer(refs) { } }); - // Delegated context menu - mainRendererReferences.chatMessagesDiv.addEventListener('contextmenu', (e) => { + // Delegated context menu + if (features.contextMenu) ownRendererListener(mainRendererReferences.chatMessagesDiv, 'contextmenu', (e) => { const messageItem = e.target.closest('.message-item'); if (!messageItem) return; @@ -2448,8 +2547,8 @@ function initializeMessageRenderer(refs) { } }); - // Delegated middle mouse button click - mainRendererReferences.chatMessagesDiv.addEventListener('mousedown', (e) => { + // Delegated middle mouse button click + if (features.middleClick) ownRendererListener(mainRendererReferences.chatMessagesDiv, 'mousedown', (e) => { if (e.button !== 1) return; // 只处理中键 const messageItem = e.target.closest('.message-item'); @@ -2483,42 +2582,48 @@ function initializeMessageRenderer(refs) { contentProcessor.processRenderedContent(contentDiv, globalSettings); }; - contextMenu.initializeContextMenu(mainRendererReferences, { + if (features.contextMenu) contextMenu.initializeContextMenu(mainRendererReferences, { removeMessageById: removeMessageById, - finalizeStreamedMessage: finalizeStreamedMessage, - renderMessage: renderMessage, - startStreamingMessage: startStreamingMessage, - setContentAndProcessImages: setContentAndProcessImages, + renderMessage: renderMessage, + discardStreamingMessage: streamManager.discardStreamingMessage, + setContentAndProcessImages: imageHandler.setContentAndProcessImages, processRenderedContent: wrappedProcessRenderedContent, runTextHighlights: contentProcessor.highlightAllPatternsInMessage, preprocessFullContent: preprocessFullContent, renderAttachments: renderAttachments, - interruptHandler: mainRendererReferences.interruptHandler, - updateMessageContent: updateMessageContent, // 🟢 新增:传递 updateMessageContent + interruptHandler: mainRendererReferences.interruptHandler, + showForwardModal: mainRendererReferences.showForwardModal, + ensureAudioContext: mainRendererReferences.ensureAudioContext, + updateMessageContent: updateMessageContent, // 🟢 新增:传递 updateMessageContent extractSpeakableTextFromContentElement: extractSpeakableTextFromContentElement, }); - if (typeof contextMenu.toggleEditMode === 'function') { - window.toggleEditMode = contextMenu.toggleEditMode; - window.messageContextMenu = contextMenu; - } - - streamManager.initStreamManager({ + if (features.streamProjection) streamManager.attachStreamProjection({ + chatDomRenderer: mainRendererReferences.chatDomRenderer, globalSettingsRef: mainRendererReferences.globalSettingsRef, - currentChatHistoryRef: mainRendererReferences.currentChatHistoryRef, + currentChatHistoryRef: mainRendererReferences.currentChatHistoryRef, currentSelectedItemRef: mainRendererReferences.currentSelectedItemRef, - currentTopicIdRef: mainRendererReferences.currentTopicIdRef, - chatMessagesDiv: mainRendererReferences.chatMessagesDiv, + currentTopicIdRef: mainRendererReferences.currentTopicIdRef, + transientStreamHistory: mainRendererReferences.transientStreamHistory, + viewAuthority: { + isCurrent: context => { + const selected = mainRendererReferences.currentSelectedItemRef.get(); + const topicId = mainRendererReferences.currentTopicIdRef.get(); + const itemId = context?.groupId || context?.agentId; + return Boolean(selected?.id && topicId && itemId === selected.id && context?.topicId === topicId); + } + }, + chatMessagesDiv: mainRendererReferences.chatMessagesDiv, parseTail: parseStreamTailMarkdown, parseFull: parseFullMarkdown, prepareFinalTextForRender: prepareFinalTextForRender, renderMermaidDiagrams: renderMermaidDiagrams, - electronAPI: mainRendererReferences.electronAPI, - uiHelper: mainRendererReferences.uiHelper, - morphdom: window.morphdom, + electronAPI: mainRendererReferences.electronAPI, + uiHelper: mainRendererReferences.uiHelper, + morphdom: mainRendererReferences.morphdom, renderMessage: renderMessage, showContextMenu: contextMenu.showContextMenu, - setContentAndProcessImages: setContentAndProcessImages, + setContentAndProcessImages: imageHandler.setContentAndProcessImages, processRenderedContent: wrappedProcessRenderedContent, runTextHighlights: contentProcessor.highlightAllPatternsInMessage, preprocessFullContent: preprocessFullContent, @@ -2531,7 +2636,7 @@ function initializeMessageRenderer(refs) { deIndentMisinterpretedCodeBlocks: contentProcessor.deIndentMisinterpretedCodeBlocks, // 🟢 传递新函数 processStartEndMarkers: contentProcessor.processStartEndMarkers, // 🟢 传递安全处理函数 ensureSeparatorBetweenImgAndCode: contentProcessor.ensureSeparatorBetweenImgAndCode, - processAnimationsInContent: processAnimationsInContent, + processAnimationsInContent: contentDiv => processAnimationsInContent(contentDiv, visibilityOptimizer), renderPostProcessedHtml: renderPostProcessedHtml, emoticonUrlFixer: emoticonUrlFixer, // 🟢 Pass emoticon fixer for live updates enhancedRenderDebounceTimers: enhancedRenderDebounceTimers, @@ -2539,12 +2644,17 @@ function initializeMessageRenderer(refs) { DIARY_RENDER_DEBOUNCE_DELAY: DIARY_RENDER_DEBOUNCE_DELAY, }); - middleClickHandler.initialize(mainRendererReferences, { - removeMessageById: removeMessageById, - }); + if (features.middleClick) middleClickHandler.initialize(mainRendererReferences, { + removeMessageById: removeMessageById, + streamManager, + toggleEditMode: contextMenu.toggleEditMode, + handleRegenerateResponse: contextMenu.handleRegenerateResponse, + showForwardModal: mainRendererReferences.showForwardModal, + ensureAudioContext: mainRendererReferences.ensureAudioContext, + }); // --- 用户气泡文件拖拽支持 --- - mainRendererReferences.chatMessagesDiv.addEventListener('dragover', (e) => { + ownRendererListener(mainRendererReferences.chatMessagesDiv, 'dragover', (e) => { const messageItem = e.target.closest('.message-item.user'); if (!messageItem) return; @@ -2563,7 +2673,7 @@ function initializeMessageRenderer(refs) { } }); - mainRendererReferences.chatMessagesDiv.addEventListener('dragleave', (e) => { + ownRendererListener(mainRendererReferences.chatMessagesDiv, 'dragleave', (e) => { const messageItem = e.target.closest('.message-item.user'); if (!messageItem) return; @@ -2577,7 +2687,7 @@ function initializeMessageRenderer(refs) { } }); - mainRendererReferences.chatMessagesDiv.addEventListener('drop', async (e) => { + ownRendererListener(mainRendererReferences.chatMessagesDiv, 'drop', async (e) => { const messageItem = e.target.closest('.message-item.user'); if (!messageItem) return; @@ -2594,22 +2704,22 @@ function initializeMessageRenderer(refs) { console.log(`[MessageRenderer] Drop detected on message ${messageId}. Files count: ${files?.length || 0}`); if (files && files.length > 0) { - if (window.chatManager && window.chatManager.processFilesData) { + if (mainRendererReferences.messageCommands.processFilesData) { // 使用通用的文件读取管线 - const processedFiles = await window.chatManager.processFilesData(files); + const processedFiles = await mainRendererReferences.messageCommands.processFilesData(files); const successfulFiles = processedFiles.filter(f => !f.error); if (successfulFiles.length > 0) { - window.chatManager.addAttachmentsToMessage(messageId, successfulFiles); + mainRendererReferences.messageCommands.addAttachmentsToMessage(messageId, successfulFiles); } else if (processedFiles.length > 0) { const firstError = processedFiles.find(f => f.error)?.error; console.error(`[MessageRenderer] All files failed to process: ${firstError}`); - if (window.uiHelperFunctions && window.uiHelperFunctions.showToastNotification) { - window.uiHelperFunctions.showToastNotification(`读取文件失败: ${firstError}`, 'error'); + if (mainRendererReferences.uiHelper?.showToastNotification) { + mainRendererReferences.uiHelper.showToastNotification(`读取文件失败: ${firstError}`, 'error'); } } - } else { - console.error('[MessageRenderer] window.chatManager.processFilesData not available!'); + } else { + console.error('[MessageRenderer] chat manager file capability is unavailable.'); } } }); @@ -2672,7 +2782,8 @@ function getAudioDisplayName(audio) { if (!source) return '音频'; try { - const url = new URL(source, window.location.href); + const ownerWindow = audio.ownerDocument?.defaultView; + const url = new URL(source, ownerWindow?.location?.href || 'about:blank'); const fileName = decodeURIComponent(url.pathname.split('/').filter(Boolean).pop() || ''); return fileName || '音频'; } catch (error) { @@ -2680,8 +2791,8 @@ function getAudioDisplayName(audio) { } } -function createAudioControlButton(className, label, iconMarkup) { - const button = document.createElement('button'); +function createAudioControlButton(ownerDocument, className, label, iconMarkup) { + const button = ownerDocument.createElement('button'); button.type = 'button'; button.className = `vcp-audio-button ${className}`; button.setAttribute('aria-label', label); @@ -2690,8 +2801,10 @@ function createAudioControlButton(className, label, iconMarkup) { return button; } -function enhanceAudioPlayers(container) { - if (!container) return; +function enhanceAudioPlayers(container) { + if (!container) return; + const ownerDocument = container.ownerDocument; + if (!ownerDocument) return; const playIcon = ` - const styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.setAttribute('data-chat-scope-id', chatScopeId); - styleElement.textContent = scopedChatCss; - document.head.appendChild(styleElement); + const styleElement = ownerDocument.createElement('style'); + styleElement.type = 'text/css'; + styleElement.setAttribute('data-chat-scope-id', chatScopeId); + styleElement.setAttribute('data-vcp-surface-id', surfaceId); + styleElement.textContent = scopedChatCss; + ownerDocument.head.appendChild(styleElement); + ownedStyleElements.add(styleElement); } } } @@ -3450,27 +3619,12 @@ async function renderMessage(message, isInitialLoad = false, appendToDom = true, // Attachments and content processing are now deferred within a requestAnimationFrame // to prevent race conditions during history loading. See the block above. - // The responsibility of updating the history array is now moved to the caller (e.g., chatManager.handleSendMessage) - // to ensure a single source of truth and prevent race conditions. - /* - if (!isInitialLoad && !message.isThinking) { - const currentChatHistoryArray = mainRendererReferences.currentChatHistoryRef.get(); - currentChatHistoryArray.push(message); - mainRendererReferences.currentChatHistoryRef.set(currentChatHistoryArray); // Update the ref - - if (currentSelectedItem.id && mainRendererReferences.currentTopicIdRef.get()) { - if (currentSelectedItem.type === 'agent') { - electronAPI.saveChatHistory(currentSelectedItem.id, mainRendererReferences.currentTopicIdRef.get(), currentChatHistoryArray); - } else if (currentSelectedItem.type === 'group') { - // Group history is usually saved by groupchat.js in main process after AI response - } - } - } - */ + // History mutation belongs to the caller's mutation authority. This renderer + // only projects the message and updates the in-memory view when requested. if (isInitialLoad && message.isThinking && !isActiveStreamRequest) { // 仅清理没有对应活动请求的陈旧思考占位。 // 活动异步请求可能在用户切换 Agent/话题后重新加载,不能在这里误删。 - const currentChatHistoryArray = mainRendererReferences.currentChatHistoryRef.get(); + const currentChatHistoryArray = [...mainRendererReferences.currentChatHistoryRef.get()]; const thinkingMsgIndex = currentChatHistoryArray.findIndex(m => m.id === message.id && m.isThinking); if (thinkingMsgIndex > -1) { currentChatHistoryArray.splice(thinkingMsgIndex, 1); @@ -3497,47 +3651,13 @@ function discardStreamingMessage(messageId) { } -function appendStreamChunk(messageId, chunkData, context) { - streamManager.appendStreamChunk(messageId, chunkData, context); -} - -/** - * 从完整的消息内容中提取桌面推送块,一次性推送到桌面画布 - * 仅作为兜底机制:当流式推送不可用时(如桌面窗口在流式过程中不存在), - * 在finalize时补充推送。如果流式推送已经成功处理过,这里不会重复推送。 - */ -function extractAndPushDesktopBlocks(content) { - // 此函数已被流式推送(processDesktopPushToken + setInterval)取代 - // 仅在非流式场景(如历史消息重新渲染)中作为兜底 - // 流式场景下,streamManager已经在token流中完成了推送,不需要重复 - // - // 判断依据:如果桌面画布已存在挂件,说明流式推送已成功,跳过兜底 - // 目前简单处理:完全禁用兜底推送,因为流式推送已经工作 - // 未来可以加更智能的去重逻辑(基于widgetId映射) -} - -async function finalizeStreamedMessage(messageId, finishReason, context, finalPayload = null) { - // 完整最终渲染现在由 streamManager 单次完成: - // 1) prepareFinalTextForRender() 在 streamManager 内对完整文本应用前端正则与深度; - // 2) parseFull() 只执行一次完整管线; - // 3) mermaid 也只在该最终渲染路径中执行一次。 - // 必须透传最终落盘结果,Flowlock 等消息级状态机需要解析完整原始文本。 - const finalizedMessage = await streamManager.finalizeStreamedMessage( - messageId, - finishReason, - context, - finalPayload - ); - - const finalMessage = mainRendererReferences.currentChatHistoryRef.get().find(m => m.id === messageId); - if (finalMessage) { - extractAndPushDesktopBlocks(finalMessage.content); - } - - return finalizedMessage; -} - - +function appendStreamChunk(messageId, chunkData, context) { + streamManager.appendStreamChunk(messageId, chunkData, context); +} + +function projectStreamTerminal(messageId, finishReason, context, finalPayload = null) { + return streamManager.projectStreamTerminal(messageId, finishReason, context, finalPayload); +} /** * Renders a full, non-streamed message, replacing a 'thinking' placeholder. @@ -3546,9 +3666,10 @@ async function finalizeStreamedMessage(messageId, finishReason, context, finalPa * @param {string} agentName - The name of the agent sending the message. * @param {string} agentId - The ID of the agent sending the message. */ -async function renderFullMessage(messageId, fullContent, agentName, agentId) { +async function renderFullMessage(messageId, fullContent, agentName, agentId, options = {}) { console.debug(`[MessageRenderer renderFullMessage] Rendering full message for ID: ${messageId}`); - const { chatMessagesDiv, electronAPI, uiHelper, markedInstance } = mainRendererReferences; + const { chatMessagesDiv: defaultChatMessagesDiv, electronAPI, uiHelper, markedInstance } = mainRendererReferences; + const chatMessagesDiv = options.root || defaultChatMessagesDiv; const currentChatHistoryArray = mainRendererReferences.currentChatHistoryRef.get(); const currentSelectedItem = mainRendererReferences.currentSelectedItemRef.get(); const currentTopicIdVal = mainRendererReferences.currentTopicIdRef.get(); @@ -3565,10 +3686,15 @@ async function renderFullMessage(messageId, fullContent, agentName, agentId) { mainRendererReferences.currentChatHistoryRef.set([...currentChatHistoryArray]); // Save history - if (currentSelectedItem && currentSelectedItem.id && currentTopicIdVal && currentSelectedItem.type === 'group') { - if (electronAPI.saveGroupChatHistory) { - try { - await electronAPI.saveGroupChatHistory(currentSelectedItem.id, currentTopicIdVal, currentChatHistoryArray.filter(m => !m.isThinking)); + if (options.persistHistory !== false && currentSelectedItem && currentSelectedItem.id && currentTopicIdVal && currentSelectedItem.type === 'group') { + if (mainRendererReferences.chatRepository) { + try { + await mainRendererReferences.historyMutationAuthority.replace({ + itemId: currentSelectedItem.id, + itemType: currentSelectedItem.type, + topicId: currentTopicIdVal, + category: 'non-stream-terminal', + }, currentChatHistoryArray.filter(m => !m.isThinking)); } catch (error) { console.error(`[MR renderFullMessage] FAILED to save GROUP history for ${currentSelectedItem.id}, topic ${currentTopicIdVal}:`, error); } @@ -3586,7 +3712,7 @@ async function renderFullMessage(messageId, fullContent, agentName, agentId) { } messageItem.classList.remove('thinking', 'streaming'); - window.updateSendButtonState?.(); + mainRendererReferences.messageCommands.updateSendButtonState?.(); const contentDiv = messageItem.querySelector('.md-content'); if (!contentDiv) { @@ -3597,7 +3723,7 @@ async function renderFullMessage(messageId, fullContent, agentName, agentId) { // Update timestamp display if it was missing const nameTimeBlock = messageItem.querySelector('.name-time-block'); if (nameTimeBlock && !nameTimeBlock.querySelector('.message-timestamp')) { - const timestampDiv = document.createElement('div'); + const timestampDiv = messageItem.ownerDocument.createElement('div'); timestampDiv.classList.add('message-timestamp'); const messageFromHistory = currentChatHistoryArray.find(m => m.id === messageId); timestampDiv.textContent = formatMessageTimestamp(messageFromHistory?.timestamp || Date.now()); @@ -3645,24 +3771,42 @@ async function renderFullMessage(messageId, fullContent, agentName, agentId) { mainRendererReferences.uiHelper.scrollToBottom(); } -function scheduleMessagePretextEstimate(messageId, text, container) { - if (!window.pretextBridge || !window.pretextBridge.isReady() || !messageId || !text) return; +function scheduleMessagePretextEstimate(messageId, text, container) { + if (!mainRendererReferences.pretextBridge || !mainRendererReferences.pretextBridge.isReady() || !messageId || !text) return; const run = () => { try { const containerWidth = container ? container.clientWidth : 800; - window.pretextBridge.estimateHeight(messageId, text, 'body', containerWidth); + mainRendererReferences.pretextBridge.estimateHeight(messageId, text, 'body', containerWidth); } catch (e) { // Pretext 失败不影响正常渲染 } }; - if (typeof window.requestIdleCallback === 'function') { - window.requestIdleCallback(run, { timeout: 300 }); - } else { - setTimeout(run, 0); - } -} + const contentDiv = container?.closest?.('.md-content') || container?.querySelector?.('.md-content') || null; + if (contentDiv?._vcpPretextIdleHandle) { + const previous = contentDiv._vcpPretextIdleHandle; + const ownerWindow = contentDiv.ownerDocument?.defaultView; + if (previous.kind === 'idle' && typeof ownerWindow?.cancelIdleCallback === 'function') ownerWindow.cancelIdleCallback(previous.id); + else if (previous.kind === 'timer') clearTimeout(previous.id); + } + const wrappedRun = () => { + if (contentDiv) delete contentDiv._vcpPretextIdleHandle; + run(); + }; + const ownerWindow = contentDiv?.ownerDocument?.defaultView; + if (typeof ownerWindow?.requestIdleCallback === 'function') { + const id = ownerWindow.requestIdleCallback(wrappedRun, { timeout: 300 }); + if (contentDiv) contentDiv._vcpPretextIdleHandle = { kind: 'idle', id }; + } else { + const id = ownerWindow?.setTimeout?.(wrappedRun, 0) || setTimeout(wrappedRun, 0); + if (contentDiv) contentDiv._vcpPretextIdleHandle = { kind: 'timer', id }; + } +} + +async function renderFullMessageProjection(messageId, fullContent, agentName, agentId, root = mainRendererReferences.chatMessagesDiv) { + return renderFullMessage(messageId, fullContent, agentName, agentId, { persistHistory: false, root }); +} function updateMessageContent(messageId, newContent) { const { chatMessagesDiv, markedInstance, globalSettingsRef } = mainRendererReferences; @@ -3757,8 +3901,8 @@ function prepareUserMessageText(text) { * @param {number} options.batchSize - Size of subsequent batches (default: 10) * @param {number} options.batchDelay - Delay between batches in ms (default: 100) */ -async function renderHistory(history, options = {}) { - const renderSessionId = invalidateRenderSession(); +async function renderHistory(history, options = {}) { + const renderSessionId = invalidateRenderSession(options.root || mainRendererReferences.chatMessagesDiv); const { initialBatch = 5, @@ -3773,9 +3917,10 @@ async function renderHistory(history, options = {}) { return Promise.resolve(); } - const renderContext = { - depthMap: buildTurnDepthMap(history) - }; + const renderContext = { + depthMap: buildTurnDepthMap(history), + root: options.root || null + }; // 如果消息数量很少,直接使用原来的方式渲染 if (history.length <= initialBatch) { @@ -3837,10 +3982,12 @@ function processDeferredMessageElement(el, renderSessionId, renderContext = {}) delete el._vcp_renderSessionId; } -async function renderMessageBatch(messages, scrollToBottom = false, renderSessionId = getActiveRenderSessionId(), renderContext = {}) { +async function renderMessageBatch(messages, scrollToBottom = false, renderSessionId = null, renderContext = {}) { + renderSessionId ||= getActiveRenderSessionId(renderContext.root || mainRendererReferences.chatMessagesDiv); if (!isRenderSessionActive(renderSessionId)) return; - const fragment = document.createDocumentFragment(); + const renderRoot = renderContext.root || mainRendererReferences.chatMessagesDiv; + const fragment = renderRoot.ownerDocument.createDocumentFragment(); const messageElements = []; // 使用 Promise.allSettled 避免单个失败影响整体 @@ -3862,17 +4009,13 @@ async function renderMessageBatch(messages, scrollToBottom = false, renderSessio // 一次性添加到 fragment messageElements.forEach(el => fragment.appendChild(el)); - // 使用 requestAnimationFrame 确保 DOM 更新不阻塞 UI - return new Promise(resolve => { - requestAnimationFrame(() => { - if (!isRenderSessionActive(renderSessionId)) { - resolve(); - return; - } + // 使用 owner-managed animation frame,Surface teardown 会取消并等待它。 + return renderTaskOwner.animationFrame(renderRoot, () => { + if (!isRenderSessionActive(renderSessionId)) return; // Step 1: Append all elements to the DOM at once. - mainRendererReferences.chatMessagesDiv.appendChild(fragment); - window.chatManager?.syncNextUiEmptyStateWithMessages?.(); + renderRoot.appendChild(fragment); + mainRendererReferences.messageCommands.syncNextUiEmptyStateWithMessages?.(); // Step 2: Now that they are in the DOM, run the deferred processing for each. messageElements.forEach(el => processDeferredMessageElement(el, renderSessionId, renderContext)); @@ -3880,9 +4023,8 @@ async function renderMessageBatch(messages, scrollToBottom = false, renderSessio if (scrollToBottom && isRenderSessionActive(renderSessionId)) { mainRendererReferences.uiHelper.scrollToBottom(); } - resolve(); - }); - }); + return messageElements; + }); } /** @@ -3894,7 +4036,8 @@ async function renderMessageBatch(messages, scrollToBottom = false, renderSessio /** * 智能批量渲染:使用 requestIdleCallback 在浏览器空闲时渲染 */ -async function renderOlderMessagesInBatches(olderMessages, batchSize, batchDelay, renderSessionId = getActiveRenderSessionId(), renderContext = {}) { +async function renderOlderMessagesInBatches(olderMessages, batchSize, batchDelay, renderSessionId = null, renderContext = {}) { + renderSessionId ||= getActiveRenderSessionId(renderContext.root || mainRendererReferences.chatMessagesDiv); const totalBatches = Math.ceil(olderMessages.length / batchSize); for (let i = totalBatches - 1; i >= 0; i--) { @@ -3905,7 +4048,8 @@ async function renderOlderMessagesInBatches(olderMessages, batchSize, batchDelay const batch = olderMessages.slice(startIndex, endIndex); // 创建批次 fragment - const batchFragment = document.createDocumentFragment(); + const renderRoot = renderContext.root || mainRendererReferences.chatMessagesDiv; + const batchFragment = renderRoot.ownerDocument.createDocumentFragment(); const elementsForProcessing = []; for (const msg of batch) { @@ -3918,49 +4062,36 @@ async function renderOlderMessagesInBatches(olderMessages, batchSize, batchDelay } } - // 🟢 使用 requestIdleCallback 在空闲时插入(降级到 requestAnimationFrame) - await new Promise(resolve => { - const insertBatch = () => { - if (!isRenderSessionActive(renderSessionId)) { - resolve(); - return; - } - - const chatMessagesDiv = mainRendererReferences.chatMessagesDiv; - let insertPoint = chatMessagesDiv.firstChild; - while (insertPoint?.classList?.contains('topic-timestamp-bubble')) { - insertPoint = insertPoint.nextSibling; - } - - if (insertPoint) { - chatMessagesDiv.insertBefore(batchFragment, insertPoint); - } else { - chatMessagesDiv.appendChild(batchFragment); - } - window.chatManager?.syncNextUiEmptyStateWithMessages?.(); - - elementsForProcessing.forEach(el => processDeferredMessageElement(el, renderSessionId, { - ...renderContext, - deferHeavy: true - })); - - resolve(); - }; - - // 优先使用 requestIdleCallback,不支持时降级到 rAF - if ('requestIdleCallback' in window) { - requestIdleCallback(insertBatch, { timeout: 1000 }); - } else { - requestAnimationFrame(insertBatch); - } - }); + // 🟢 owner-managed idle work,当前 root revoke/dispose 时会被取消。 + await renderTaskOwner.idle(renderRoot, () => { + if (!isRenderSessionActive(renderSessionId)) return; + + let insertPoint = renderRoot.firstChild; + while (insertPoint?.classList?.contains('topic-timestamp-bubble')) { + insertPoint = insertPoint.nextSibling; + } + + if (insertPoint) { + renderRoot.insertBefore(batchFragment, insertPoint); + } else { + renderRoot.appendChild(batchFragment); + } + mainRendererReferences.messageCommands.syncNextUiEmptyStateWithMessages?.(); + + elementsForProcessing.forEach(el => processDeferredMessageElement(el, renderSessionId, { + ...renderContext, + deferHeavy: true + })); + }, { timeout: 1000 }); if (!isRenderSessionActive(renderSessionId)) return; // 动态调整延迟:如果批次小,减少延迟 if (i > 0 && batchDelay > 0) { const actualDelay = batch.length < batchSize / 2 ? batchDelay / 2 : batchDelay; - await new Promise(resolve => setTimeout(resolve, actualDelay)); + const ownerWindow = renderRoot.ownerDocument?.defaultView; + if (typeof ownerWindow?.setTimeout !== 'function') return; + await new Promise(resolve => ownerWindow.setTimeout(resolve, actualDelay)); } } } @@ -3969,10 +4100,12 @@ async function renderOlderMessagesInBatches(olderMessages, batchSize, batchDelay * 原始的历史渲染方法(用于少量消息的情况) * @param {Array} history 聊天历史 */ -async function renderHistoryLegacy(history, renderSessionId = getActiveRenderSessionId(), renderContext = {}) { +async function renderHistoryLegacy(history, renderSessionId = null, renderContext = {}) { + renderSessionId ||= getActiveRenderSessionId(renderContext.root || mainRendererReferences.chatMessagesDiv); if (!isRenderSessionActive(renderSessionId)) return; - const fragment = document.createDocumentFragment(); + const renderRoot = renderContext.root || mainRendererReferences.chatMessagesDiv; + const fragment = renderRoot.ownerDocument.createDocumentFragment(); const allMessageElements = []; // Phase 1: Create all message elements in memory without appending to DOM @@ -3990,16 +4123,12 @@ async function renderHistoryLegacy(history, renderSessionId = getActiveRenderSes // Phase 2: Append all created elements at once using a DocumentFragment allMessageElements.forEach(el => fragment.appendChild(el)); - return new Promise(resolve => { - requestAnimationFrame(() => { - if (!isRenderSessionActive(renderSessionId)) { - resolve(); - return; - } + return renderTaskOwner.animationFrame(renderRoot, () => { + if (!isRenderSessionActive(renderSessionId)) return; // Step 1: Append all elements to the DOM. - mainRendererReferences.chatMessagesDiv.appendChild(fragment); - window.chatManager?.syncNextUiEmptyStateWithMessages?.(); + renderRoot.appendChild(fragment); + mainRendererReferences.messageCommands.syncNextUiEmptyStateWithMessages?.(); // Step 2: Run the deferred processing for each element now that it's attached. allMessageElements.forEach(el => processDeferredMessageElement(el, renderSessionId, renderContext)); @@ -4007,9 +4136,8 @@ async function renderHistoryLegacy(history, renderSessionId = getActiveRenderSes if (isRenderSessionActive(renderSessionId)) { mainRendererReferences.uiHelper.scrollToBottom(); } - resolve(); - }); - }); + return allMessageElements; + }); } function refreshLayoutDependentState() { @@ -4021,13 +4149,13 @@ function refreshLayoutDependentState() { messageItem.style.containIntrinsicSize = 'auto 100px'; }); - requestAnimationFrame(() => { - if (!chatMessagesDiv.isConnected) return; - visibilityOptimizer.recheckVisibility(); - }); -} + void renderTaskOwner.animationFrame(chatMessagesDiv, () => { + if (!chatMessagesDiv.isConnected) return; + visibilityOptimizer.recheckVisibility(); + }).catch(error => console.warn('[MessageRenderer] layout refresh failed:', error)); +} -window.messageRenderer = { +const messageRenderer = { initializeMessageRenderer, setCurrentSelectedItem, // Keep for renderer.js to call setCurrentTopicId, // Keep for renderer.js to call @@ -4040,24 +4168,28 @@ window.messageRenderer = { renderHistoryLegacy, // Expose the legacy rendering for compatibility renderMessageBatch, // Expose batch rendering utility startStreamingMessage, - discardStreamingMessage, - appendStreamChunk, - finalizeStreamedMessage, - renderFullMessage, + discardStreamingMessage, + appendStreamChunk, + projectStreamTerminal, + renderFullMessage, + renderFullMessageProjection, clearChat, removeMessageById, updateMessageContent, // Expose the new function refreshLayoutDependentState, extractSpeakableTextFromContentElement, - clearRenderHtmlCache, + clearRenderHtmlCache, + disposeRendererListeners, + disposeRendererResources, + disposeRootResources, + createDomRenderer: (root = mainRendererReferences.chatMessagesDiv) => createChatDomRenderer({ root, renderer: messageRenderer }), getRenderHtmlCacheStats: () => ({ entries: renderHtmlCache.size, bytes: renderHtmlCacheBytes, ...renderHtmlCacheStats }), - updateMessageUI: async (messageId, updatedMessage) => { - const { chatMessagesDiv } = mainRendererReferences; - const existingMessageDom = chatMessagesDiv.querySelector(`.message-item[data-message-id="${messageId}"]`); + updateMessageUI: async (messageId, updatedMessage, root = mainRendererReferences.chatMessagesDiv) => { + const existingMessageDom = root?.querySelector?.(`.message-item[data-message-id="${messageId}"]`); if (!existingMessageDom) return; const newMessageDom = await renderMessage(updatedMessage, true, false); if (newMessageDom) { @@ -4072,9 +4204,9 @@ window.messageRenderer = { } } }, - isMessageInitialized: (messageId) => { + isMessageInitialized: (messageId, root = mainRendererReferences.chatMessagesDiv) => { // Check if message exists in DOM or is being tracked by streamManager - const messageInDom = mainRendererReferences.chatMessagesDiv?.querySelector(`.message-item[data-message-id="${messageId}"]`); + const messageInDom = root?.querySelector?.(`.message-item[data-message-id="${messageId}"]`); if (messageInDom) return true; // Also check if streamManager is tracking this message @@ -4100,4 +4232,7 @@ window.messageRenderer = { console.error("contextMenu or setContextMenuDependencies not available."); } } -}; +}; + +return Object.freeze(messageRenderer); +} diff --git a/modules/notificationRenderer.js b/modules/notificationRenderer.js index f1662eddf..f5bc5ffdd 100755 --- a/modules/notificationRenderer.js +++ b/modules/notificationRenderer.js @@ -1,6 +1,24 @@ // modules/notificationRenderer.js -var notificationRendererApi = window.chatAPI || window.electronAPI; +var notificationRendererApi = window.chatAPI || window.electronAPI; +let filterManagerCapability = null; +let notificationLifecycleOwner = null; + +function checkMessageFilter(messageTitle) { + return filterManagerCapability?.checkMessageFilter?.(messageTitle) || null; +} + +function scheduleNotificationTimeout(callback, delay) { + return notificationLifecycleOwner?.timeout + ? notificationLifecycleOwner.timeout(callback, delay) + : setTimeout(callback, delay); +} + +function listenNotification(target, type, handler, options) { + if (notificationLifecycleOwner?.add) return notificationLifecycleOwner.add(target, type, handler, options); + target?.addEventListener?.(type, handler, options); + return true; +} /** * @typedef {Object} VCPLogStatus @@ -77,7 +95,7 @@ function sendToolApprovalResponse(requestId, approved, reason = '') { */ function renderVCPLogNotification(logData, originalRawMessage = null, notificationsListUl, themeColors = {}) { if (logData && typeof logData === 'object' && logData.type === 'tool_approval_request' && logData.data && typeof logData.data === 'object') { - const autoApprovalResult = window.filterManager?.checkToolAutoApproval?.(logData.data); + const autoApprovalResult = (filterManagerCapability || window.filterManager)?.checkToolAutoApproval?.(logData.data); if (autoApprovalResult && autoApprovalResult.action === 'approve') { const sent = sendToolApprovalResponse(logData.data.requestId, true); const autoApprovalLog = { @@ -372,7 +390,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati const originalMarkup = copyButton.innerHTML; copyButton.textContent = '已复制!'; copyButton.disabled = true; - setTimeout(() => { + scheduleNotificationTimeout(() => { copyButton.textContent = originalText; copyButton.innerHTML = originalMarkup; copyButton.disabled = false; @@ -382,7 +400,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati const originalText = copyButton.textContent; const originalMarkup = copyButton.innerHTML; copyButton.textContent = '错误!'; - setTimeout(() => { + scheduleNotificationTimeout(() => { copyButton.textContent = originalText; copyButton.innerHTML = originalMarkup; }, 1500); @@ -397,7 +415,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati element.style.opacity = '0'; element.style.transform = 'translateX(100%)'; // Assuming this is the desired animation for list items - setTimeout(() => { + scheduleNotificationTimeout(() => { if (element.parentNode) { element.parentNode.removeChild(element); } @@ -410,13 +428,13 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati toastElement.classList.add('exiting'); // 设置一个fallback timeout,确保元素一定会被移除 - const fallbackTimeout = setTimeout(() => { + const fallbackTimeout = scheduleNotificationTimeout(() => { if (toastElement.parentNode) { toastElement.parentNode.removeChild(toastElement); } }, 500); // 500ms后强制移除,即使transition没有完成 - toastElement.addEventListener('transitionend', () => { + listenNotification(toastElement, 'transitionend', () => { clearTimeout(fallbackTimeout); // 如果transition正常完成,清除fallback if (toastElement.parentNode) { toastElement.parentNode.removeChild(toastElement); @@ -440,7 +458,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati } else { approvalElement.style.opacity = '0'; approvalElement.style.transform = 'translateX(100%)'; - setTimeout(() => approvalElement.remove(), 500); + scheduleNotificationTimeout(() => approvalElement.remove(), 500); } }); }; @@ -465,7 +483,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati populateNotificationElement(toastBubble, true); toastContainer.prepend(toastBubble); - setTimeout(() => toastBubble.classList.add('visible'), 50); + scheduleNotificationTimeout(() => toastBubble.classList.add('visible'), 50); // 增强自动消失逻辑,支持自定义停留时间 let autoDismissDelay = 7000; // 默认7秒 @@ -473,8 +491,8 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati // 审核类通知永不自动消失 if (isToolApprovalRequest) { autoDismissDelay = Infinity; - } else if (typeof window.checkMessageFilter === 'function') { - const filterResult = window.checkMessageFilter(titleText); + } else { + const filterResult = checkMessageFilter(titleText); if (filterResult && filterResult.duration !== undefined) { autoDismissDelay = filterResult.duration === 0 ? Infinity : filterResult.duration * 1000; } @@ -485,7 +503,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati // 永久显示,不设置自动消失定时器 autoDismissTimeout = null; } else { - autoDismissTimeout = setTimeout(() => { + autoDismissTimeout = scheduleNotificationTimeout(() => { if (toastBubble.parentNode && toastBubble.classList.contains('visible') && !toastBubble.classList.contains('exiting')) { closeToastNotification(toastBubble); } @@ -511,7 +529,7 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati populateNotificationElement(listItemBubble, false); notificationsListUl.prepend(listItemBubble); // Apply 'visible' class for potential animations on list items if defined in CSS - setTimeout(() => listItemBubble.classList.add('visible'), 50); + scheduleNotificationTimeout(() => listItemBubble.classList.add('visible'), 50); } else { console.warn('Notifications sidebar UL not found. Persistent notification not added.'); } @@ -520,12 +538,24 @@ function renderVCPLogNotification(logData, originalRawMessage = null, notificati // 添加窗口焦点变化监听,清理残留的通知元素 let focusCleanupInitialized = false; -function initializeFocusCleanup() { - if (focusCleanupInitialized) return; - focusCleanupInitialized = true; +function initializeFocusCleanup(options = {}) { + if (focusCleanupInitialized) return; + focusCleanupInitialized = true; + notificationLifecycleOwner = options.owner || notificationLifecycleOwner; + + const cleanupExpiredToasts = (maxAgeMs) => { + const toastContainer = document.getElementById('floating-toast-notifications-container'); + if (!toastContainer) return; + toastContainer.querySelectorAll('.floating-toast-notification').forEach(toast => { + if (toast.dataset.protectedNotification === 'tool-approval') return; + const createdAt = Number(toast.dataset.createdAt || Date.now()); + if (!toast.dataset.createdAt) toast.dataset.createdAt = String(createdAt); + if (Date.now() - createdAt > maxAgeMs) toast.parentNode?.removeChild(toast); + }); + }; // 当窗口重新获得焦点时,清理所有可能残留的通知元素 - window.addEventListener('focus', () => { + const onFocus = () => { const toastContainer = document.getElementById('floating-toast-notifications-container'); if (toastContainer) { // 查找所有添加了 exiting 类但仍在 DOM 中的元素 @@ -557,40 +587,27 @@ function initializeFocusCleanup() { } }); } - }); + }; + if (notificationLifecycleOwner?.add) notificationLifecycleOwner.add(window, 'focus', onFocus); + else window.addEventListener('focus', onFocus); // 定期清理机制,每30秒检查一次 - setInterval(() => { - const toastContainer = document.getElementById('floating-toast-notifications-container'); - if (toastContainer) { - const allToasts = toastContainer.querySelectorAll('.floating-toast-notification'); - allToasts.forEach(toast => { - if (toast.dataset.protectedNotification === 'tool-approval') return; - - if (toast.dataset.createdAt) { - const createdAt = parseInt(toast.dataset.createdAt); - const now = Date.now(); - if (now - createdAt > 15000) { // 超过15秒强制清理 - console.log('[NotificationRenderer] 定期清理超时的通知元素'); - if (toast.parentNode) { - toast.parentNode.removeChild(toast); - } - } - } - }); - } - }, 30000); // 每30秒检查一次 -} + const scheduleCleanup = () => { + cleanupExpiredToasts(15000); + if (notificationLifecycleOwner?.timeout) notificationLifecycleOwner.timeout(scheduleCleanup, 30000); + else scheduleNotificationTimeout(scheduleCleanup, 30000); + }; + scheduleCleanup(); +} // Expose functions to be used by renderer.js window.notificationRenderer = { updateVCPLogStatus, renderVCPLogNotification, initializeFocusCleanup, - clearPersistentNotifications + clearPersistentNotifications, + configureCapabilities({ filterManager = null, listenerOwner = null } = {}) { + filterManagerCapability = filterManager; + notificationLifecycleOwner = listenerOwner; + } }; - -// Make globalSettings accessible for do not disturb mode check -if (typeof window.globalSettings === 'undefined') { - window.globalSettings = {}; -} diff --git a/modules/renderer/animation.js b/modules/renderer/animation.js index 3b4b753ea..228642eee 100644 --- a/modules/renderer/animation.js +++ b/modules/renderer/animation.js @@ -10,9 +10,6 @@ const CDN_TO_LOCAL_MAP = { 'https://unpkg.com/animejs': 'vendor/anime.min.js', }; -import * as visibilityOptimizer from './visibilityOptimizer.js'; -import { createPausableRAF, createPausableTimerAPI, registerCanvasAnimation } from './visibilityOptimizer.js'; - // 🔥 全局跟踪已加载的脚本,防止跨消息重复加载 if (!window._vcp_loaded_scripts) { window._vcp_loaded_scripts = new Set(); @@ -58,6 +55,7 @@ function replaceCdnUrls(scriptContent) { } const trackedThreeInstances = new Map(); +const visibilityOwnerByContent = new WeakMap(); let isThreePatched = false; function patchThreeJS() { @@ -102,6 +100,11 @@ function patchThreeJS() { renderer.dispose = function() { if (this._disposed) return; this._disposed = true; + // This observer is owned by the renderer. Disconnect it before + // releasing GPU resources so a detached canvas cannot retain the + // document or re-register itself after teardown. + this._vcpMutationObserver?.disconnect?.(); + this._vcpMutationObserver = null; if (originalDispose) { return originalDispose.call(this); } @@ -111,6 +114,7 @@ function patchThreeJS() { if (document.body.contains(renderer.domElement)) { const contentDiv = renderer.domElement.closest('.md-content'); if (contentDiv) { + const visibilityOptimizer = visibilityOwnerByContent.get(contentDiv); if (!trackedThreeInstances.has(contentDiv)) { trackedThreeInstances.set(contentDiv, []); } @@ -136,6 +140,8 @@ function patchThreeJS() { } }); + renderer._vcpMutationObserver = observer; + observer.observe(document.body, { childList: true, subtree: true }); return renderer; @@ -167,7 +173,8 @@ function loadScript(src, onLoad, onError) { document.head.appendChild(scriptEl); } -function processScripts(containerElement) { +function processScripts(containerElement, visibilityOptimizer) { + if (visibilityOptimizer) visibilityOwnerByContent.set(containerElement, visibilityOptimizer); const messageItem = containerElement.closest('.message-item'); // Separate scripts by type @@ -229,16 +236,19 @@ function processScripts(containerElement) { const canvases = containerElement.querySelectorAll('canvas'); canvases.forEach(canvas => { if (messageItem) { - registerCanvasAnimation(messageItem, { canvas }); + visibilityOptimizer?.registerCanvasAnimation?.(messageItem, { canvas }); } }); // 2. 创建可暂停的 rAF 与 timer 包装器 const pausableRAF = messageItem - ? createPausableRAF(messageItem) + ? visibilityOptimizer?.createPausableRAF?.(messageItem) || window.requestAnimationFrame : window.requestAnimationFrame; const pausableTimerAPI = messageItem - ? createPausableTimerAPI(messageItem) + ? visibilityOptimizer?.createPausableTimerAPI?.(messageItem) || { + setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), + setInterval: window.setInterval.bind(window), clearInterval: window.clearInterval.bind(window) + } : { setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), @@ -451,14 +461,15 @@ function processScripts(containerElement) { } } -export function processAnimationsInContent(containerElement) { +export function processAnimationsInContent(containerElement, visibilityOptimizer = null) { if (!containerElement) return; - processScripts(containerElement); + processScripts(containerElement, visibilityOptimizer); } export function cleanupAnimationsInContent(contentDiv) { if (!contentDiv) return; + visibilityOwnerByContent.delete(contentDiv); if (window.anime) { const animatedElements = contentDiv.querySelectorAll('*'); diff --git a/modules/renderer/contentPipeline.js b/modules/renderer/contentPipeline.js index 7640ad4ae..41bcf4d2b 100644 --- a/modules/renderer/contentPipeline.js +++ b/modules/renderer/contentPipeline.js @@ -13,10 +13,7 @@ * - 业务细节(特殊块转换、HTML fenced 等)通过依赖注入提供 */ -const PIPELINE_MODES = { - FULL_RENDER: 'full-render', - STREAM_FAST: 'stream-fast' -}; +import { PIPELINE_MODES } from '../chat/contentModes.js'; function noop(value) { return value; @@ -513,4 +510,4 @@ function createContentPipeline(deps = {}) { export { PIPELINE_MODES, createContentPipeline -}; \ No newline at end of file +}; diff --git a/modules/renderer/contentProcessor.js b/modules/renderer/contentProcessor.js index 44805d9ea..9c85f55c5 100644 --- a/modules/renderer/contentProcessor.js +++ b/modules/renderer/contentProcessor.js @@ -1,6 +1,26 @@ -// modules/renderer/contentProcessor.js - -let mainRefs = {}; +// modules/renderer/contentProcessor.js + +/** Creates one rendered-content interaction owner for one MessageRenderer instance. */ +export function createContentProcessor() { +let mainRefs = {}; + +function scheduleOwnedTimeout(owner, callback, delay) { + if (!owner) return setTimeout(callback, delay); + if (!owner._vcpOwnedTimeouts) owner._vcpOwnedTimeouts = new Set(); + const timerId = setTimeout(() => { + owner._vcpOwnedTimeouts?.delete(timerId); + callback(); + }, delay); + owner._vcpOwnedTimeouts.add(timerId); + return timerId; +} + +function clearOwnedTimeouts(owner) { + if (!owner?._vcpOwnedTimeouts) return; + for (const timerId of owner._vcpOwnedTimeouts) clearTimeout(timerId); + owner._vcpOwnedTimeouts.clear(); + delete owner._vcpOwnedTimeouts; +} /** * Initializes the content processor with necessary references. @@ -546,7 +566,7 @@ function setupSingleCodeCopyButton(preElement, rawText) { copyButton.innerHTML = '⚠️失败'; } - setTimeout(() => { + scheduleOwnedTimeout(copyButton, () => { if (!copyButton.isConnected) return; copyButton.disabled = false; copyButton.classList.remove('copied', 'failed'); @@ -794,7 +814,7 @@ function setupHtmlPreview(preElement, htmlContent) { } // 🟢 延迟隐藏代码块,确保iframe先显示 - setTimeout(() => { + scheduleOwnedTimeout(preElement, () => { preElement.style.display = 'none'; }, 50); @@ -841,7 +861,8 @@ function processInteractiveButtons(contentDiv, settings = {}) { setupButtonStyle(button); // Add click event listener - button.addEventListener('click', handleAIButtonClick); + button.addEventListener('click', handleAIButtonClick); + button._vcpInteractiveCleanup = () => button.removeEventListener('click', handleAIButtonClick); console.log('[ContentProcessor] Processed interactive button:', button.textContent.trim()); }); @@ -903,7 +924,7 @@ function handleAIButtonClick(event) { disableButton(button); // Send the message asynchronously to avoid blocking - setTimeout(() => { + scheduleOwnedTimeout(button, () => { sendButtonMessage(finalSendText, button); }, 10); @@ -951,14 +972,11 @@ function restoreButton(button) { function sendButtonMessage(text, button) { try { // Check if chatManager is available - if (window.chatManager && typeof window.chatManager.handleSendMessage === 'function') { - // Use the main chat manager for regular chat - sendMessageViaMainChat(text); - } else if (window.sendMessage && typeof window.sendMessage === 'function') { - // Use direct sendMessage function (for voice chat, assistant modules) - window.sendMessage(text); - } else { - throw new Error('No message sending function available'); + if (mainRefs.messageCommands && typeof mainRefs.messageCommands.handleSendMessage === 'function') { + // Use the main chat manager for regular chat + sendMessageViaMainChat(text); + } else { + throw new Error('No message sending function available'); } console.log('[ContentProcessor] Button message sent:', text); @@ -978,19 +996,13 @@ function sendButtonMessage(text, button) { * Sends message via main chat interface * @param {string} text The text to send */ -function sendMessageViaMainChat(text) { - // Get the message input element - const messageInput = document.getElementById('messageInput'); - if (!messageInput) { - throw new Error('Message input not found'); - } - - // Set the text in input and trigger send - messageInput.value = text; - window.chatManager.handleSendMessage(); - - // Note: handleSendMessage will clear the input automatically -} +function sendMessageViaMainChat(text) { + const send = mainRefs.messageCommands?.handleSendMessage; + if (typeof send !== 'function') throw new Error('This Surface does not provide message sending'); + // The owner supplies the send capability. Never discover #messageInput + // from the document: that would route an internal Surface to main chat. + return send(text); +} /** * Shows an error notification to the user @@ -998,13 +1010,15 @@ function sendMessageViaMainChat(text) { */ function showErrorNotification(message) { // Try to use existing notification system - if (window.uiHelper && typeof window.uiHelper.showToastNotification === 'function') { - window.uiHelper.showToastNotification(message, 'error'); - return; - } - - // Fallback: create a simple notification - const notification = document.createElement('div'); + if (mainRefs.uiHelper && typeof mainRefs.uiHelper.showToastNotification === 'function') { + mainRefs.uiHelper.showToastNotification(message, 'error'); + return; + } + + // Fallback: create a simple notification + const ownerDocument = mainRefs.chatMessagesDiv?.ownerDocument; + if (!ownerDocument) return; + const notification = ownerDocument.createElement('div'); notification.textContent = message; notification.style.cssText = ` position: fixed; @@ -1019,10 +1033,10 @@ function showErrorNotification(message) { box-shadow: 0 2px 8px rgba(0,0,0,0.2); `; - document.body.appendChild(notification); + ownerDocument.body.appendChild(notification); // Auto remove after 3 seconds - setTimeout(() => { + scheduleOwnedTimeout(notification, () => { if (notification.parentNode) { document.body.removeChild(notification); } @@ -1321,8 +1335,15 @@ function deIndentMisinterpretedCodeBlocks(text) { * 清理指定容器及其子元素中所有的 HTML 预览资源(iframe、事件监听器等)。 * @param {HTMLElement} contentDiv - 存储消息内容的容器。 */ -function cleanupPreviewsInContent(contentDiv) { - if (!contentDiv) return; +function cleanupPreviewsInContent(contentDiv) { + if (!contentDiv) return; + clearOwnedTimeouts(contentDiv); + contentDiv.querySelectorAll('*').forEach(clearOwnedTimeouts); + contentDiv.querySelectorAll('button[data-vcp-interactive="true"]').forEach(button => { + button._vcpInteractiveCleanup?.(); + delete button._vcpInteractiveCleanup; + delete button.dataset.vcpInteractive; + }); const containers = contentDiv.querySelectorAll('.vcp-html-preview-container'); containers.forEach(container => { if (typeof container._vcpCleanup === 'function') { @@ -1337,8 +1358,8 @@ function cleanupPreviewsInContent(contentDiv) { } -export { - initializeContentProcessor, +const contentProcessor = Object.freeze({ + initializeContentProcessor, ensureNewlineAfterCodeBlock, ensureSpaceAfterTilde, removeIndentationFromCodeBlockMarkers, @@ -1356,5 +1377,15 @@ export { applyContentProcessors, // Export the new batch processor escapeHtml, processStartEndMarkers, - cleanupPreviewsInContent -}; + cleanupPreviewsInContent, + dispose() { + if (mainRefs.chatMessagesDiv) cleanupPreviewsInContent(mainRefs.chatMessagesDiv); + mainRefs = {}; + }, +}); +return contentProcessor; +} + +// CSS scoping is a pure transform and does not consume renderer state. +const pureContentProcessor = createContentProcessor(); +export const scopeCss = pureContentProcessor.scopeCss; diff --git a/modules/renderer/desktopPushConsumer.js b/modules/renderer/desktopPushConsumer.js new file mode 100644 index 000000000..045f10f5d --- /dev/null +++ b/modules/renderer/desktopPushConsumer.js @@ -0,0 +1,213 @@ +const START_TAG = '<<<[DESKTOP_PUSH]>>>'; +const END_TAG = '<<<[DESKTOP_PUSH_END]>>>'; +const VALID_PREFIXES = [' Date.now(), + createWidgetId = () => `dw-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 5)}`, + logger = console, + throttleMs = 100, + timeoutMs = 150_000, +} = {}) { + const states = new Map(); + let connected = false; + let disposed = false; + let unsubscribe = null; + + const resetState = state => { + state.active = false; + state.tagBuffer = ''; + state.buffer = ''; + state.widgetId = null; + state.created = false; + state.validated = false; + state.isReplaceMode = false; + state.lastPushedLength = 0; + state.lastTokenTime = null; + }; + + const stopTimer = state => { + if (!state) return; + state.timerGeneration += 1; + if (!state.pushTimer) return; + scheduler.clearInterval(state.pushTimer); + state.pushTimer = null; + }; + + const send = payload => { + if (disposed || !connected || typeof electronAPI?.desktopPush !== 'function') return; + electronAPI.desktopPush(payload); + }; + + const start = () => { + if (disposed || unsubscribe || typeof electronAPI?.onDesktopStatus !== 'function') return; + const release = electronAPI.onDesktopStatus(data => { + if (disposed) return; + connected = !!data?.connected; + logger.log?.(`[DesktopPush] Desktop window availability changed: ${connected}`); + }); + unsubscribe = typeof release === 'function' ? release : () => {}; + }; + + const processToken = (messageId, textToAppend) => { + if (disposed || !messageId || typeof textToAppend !== 'string') return textToAppend || ''; + + let state = states.get(messageId); + if (!state) { + state = { + active: false, + widgetId: null, + buffer: '', + tagBuffer: '', + created: false, + validated: false, + pushTimer: null, + lastPushedLength: 0, + lastTokenTime: null, + backtickContext: false, + isReplaceMode: false, + revoked: false, + timerGeneration: 0, + }; + states.set(messageId, state); + } + + let outputText = ''; + for (const char of textToAppend) { + if (!state.active) { + state.tagBuffer += char; + if (START_TAG.startsWith(state.tagBuffer)) { + if (state.tagBuffer === START_TAG) { + const precedingChar = outputText.length > 0 ? outputText[outputText.length - 1] : ''; + if (precedingChar === '`') { + state.backtickContext = true; + outputText += state.tagBuffer; + state.tagBuffer = ''; + continue; + } + state.active = true; + state.backtickContext = false; + state.widgetId = createWidgetId(); + state.buffer = ''; + state.created = false; + state.validated = false; + state.tagBuffer = ''; + state.lastPushedLength = 0; + } + } else { + outputText += state.tagBuffer; + state.tagBuffer = ''; + } + continue; + } + + state.tagBuffer += char; + if (END_TAG.startsWith(state.tagBuffer)) { + if (state.tagBuffer !== END_TAG) continue; + + stopTimer(state); + if (state.created) { + if (state.isReplaceMode) { + const targetMatch = state.buffer.match(/target:(?:「始ESCAPE」([\s\S]*?)「末ESCAPE」|「始」([\s\S]*?)「末」)/); + const replaceMatch = state.buffer.match(/replace:(?:「始ESCAPE」([\s\S]*?)「末ESCAPE」|「始」([\s\S]*?)「末」)/); + if (targetMatch && replaceMatch) { + const targetSelector = (targetMatch[1] || targetMatch[2] || '').trim(); + const replaceContent = (replaceMatch[1] || replaceMatch[2] || '').trim(); + send({ action: 'replace', targetSelector, content: replaceContent }); + } else { + logger.warn?.('[DesktopPush] Replace block was missing target/replace fields.'); + } + } else { + send({ action: 'append', widgetId: state.widgetId, content: state.buffer }); + send({ action: 'finalize', widgetId: state.widgetId }); + } + } + resetState(state); + continue; + } + + state.buffer += state.tagBuffer; + state.tagBuffer = ''; + state.lastTokenTime = now(); + + if (!state.validated && state.buffer.trim().length >= 5) { + const trimmedBuffer = state.buffer.trim().toLowerCase(); + const isValid = VALID_PREFIXES.some(prefix => trimmedBuffer.startsWith(prefix)); + if (isValid) { + state.validated = true; + state.isReplaceMode = trimmedBuffer.startsWith('target:'); + state.created = state.isReplaceMode + || (connected && typeof electronAPI?.desktopPush === 'function'); + + if (!state.isReplaceMode && state.created) { + send({ action: 'create', widgetId: state.widgetId, options: { x: 200, y: 150, width: 400, height: 300 } }); + } + + if (connected && typeof electronAPI?.desktopPush === 'function') { + const interval = state.isReplaceMode ? 5000 : throttleMs; + const timerGeneration = ++state.timerGeneration; + state.pushTimer = scheduler.setInterval(() => { + if (disposed || state.revoked || state.timerGeneration !== timerGeneration) return; + if (!state.isReplaceMode && state.buffer.length > state.lastPushedLength) { + send({ action: 'append', widgetId: state.widgetId, content: state.buffer }); + state.lastPushedLength = state.buffer.length; + } + if (!state.lastTokenTime || now() - state.lastTokenTime <= timeoutMs) return; + + stopTimer(state); + if (!state.isReplaceMode && state.created) { + send({ action: 'append', widgetId: state.widgetId, content: state.buffer }); + send({ action: 'finalize', widgetId: state.widgetId }); + } + resetState(state); + }, interval); + } + } else if (state.buffer.trim().length >= 30) { + logger.warn?.(`[DesktopPush] Invalid content prefix, discarding push block: "${trimmedBuffer.substring(0, 30)}..."`); + stopTimer(state); + resetState(state); + } + } + } + return outputText; + }; + + const cleanupMessage = messageId => { + const state = states.get(messageId); + if (state) state.revoked = true; + stopTimer(state); + states.delete(messageId); + }; + + const dispose = () => { + if (disposed) return; + disposed = true; + try { + unsubscribe?.(); + } catch (error) { + logger.warn?.('[DesktopPush] Status unsubscribe failed during dispose.', error); + } + unsubscribe = null; + connected = false; + for (const state of states.values()) { + state.revoked = true; + stopTimer(state); + } + states.clear(); + }; + + return Object.freeze({ + start, + processToken, + cleanupMessage, + dispose, + getStateCount: () => states.size, + }); +} diff --git a/modules/renderer/domBuilder.js b/modules/renderer/domBuilder.js index 316f2c0da..1677547e0 100644 --- a/modules/renderer/domBuilder.js +++ b/modules/renderer/domBuilder.js @@ -20,9 +20,10 @@ * }} An object containing the created DOM elements. */ -function fixVoiceChatAssetPath(url) { +function fixVoiceChatAssetPath(url, ownerWindow = null) { if (!url) return url; - const isVoiceChatPage = window.location.pathname.replace(/\\/g, '/').includes('/Voicechatmodules/'); + const pathname = ownerWindow?.location?.pathname || ''; + const isVoiceChatPage = pathname.replace(/\\/g, '/').includes('/Voicechatmodules/'); if (!isVoiceChatPage) return url; if (url.startsWith('assets/')) return `../${url}`; return url; @@ -74,8 +75,11 @@ export function applyUserMessageLayoutState(messageItem, globalSettings) { messageItem.classList.add('user-bubble-ui-disabled'); } -export function createMessageSkeleton(message, globalSettings, currentSelectedItem) { - const messageItem = document.createElement('div'); +export function createMessageSkeleton(message, globalSettings, currentSelectedItem, domRealm = {}) { + const ownerDocument = domRealm.document || domRealm.root?.ownerDocument; + if (!ownerDocument?.createElement) throw new TypeError('createMessageSkeleton requires a DOM document'); + const ownerWindow = domRealm.window || ownerDocument.defaultView; + const messageItem = ownerDocument.createElement('div'); messageItem.classList.add('message-item', message.role); if (message.isGroupMessage) messageItem.classList.add('group-message-item'); messageItem.dataset.timestamp = String(message.timestamp); @@ -83,7 +87,7 @@ export function createMessageSkeleton(message, globalSettings, currentSelectedIt if (message.agentId) messageItem.dataset.agentId = message.agentId; applyUserMessageLayoutState(messageItem, globalSettings); - const contentDiv = document.createElement('div'); + const contentDiv = ownerDocument.createElement('div'); contentDiv.classList.add('md-content'); let avatarImg = null, @@ -109,26 +113,26 @@ export function createMessageSkeleton(message, globalSettings, currentSelectedIt } if (message.role === 'user' || message.role === 'assistant') { - avatarImg = document.createElement('img'); + avatarImg = ownerDocument.createElement('img'); avatarImg.classList.add('chat-avatar'); - avatarImg.src = fixVoiceChatAssetPath(avatarUrlToUse); + avatarImg.src = fixVoiceChatAssetPath(avatarUrlToUse, ownerWindow); avatarImg.alt = `${senderNameToUse} 头像`; avatarImg.onerror = () => { avatarImg.onerror = null; - avatarImg.src = fixVoiceChatAssetPath(message.role === 'user' ? 'assets/default_user_avatar.png' : 'assets/default_avatar.png'); + avatarImg.src = fixVoiceChatAssetPath(message.role === 'user' ? 'assets/default_user_avatar.png' : 'assets/default_avatar.png', ownerWindow); }; - nameTimeDiv = document.createElement('div'); + nameTimeDiv = ownerDocument.createElement('div'); nameTimeDiv.classList.add('name-time-block'); - senderNameDiv = document.createElement('div'); + senderNameDiv = ownerDocument.createElement('div'); senderNameDiv.classList.add('sender-name'); senderNameDiv.textContent = senderNameToUse; nameTimeDiv.appendChild(senderNameDiv); if (message.timestamp && !message.isThinking) { - const timestampDiv = document.createElement('div'); + const timestampDiv = ownerDocument.createElement('div'); timestampDiv.classList.add('message-timestamp'); timestampDiv.textContent = formatMessageTimestamp(message.timestamp); nameTimeDiv.appendChild(timestampDiv); @@ -147,11 +151,4 @@ export function createMessageSkeleton(message, globalSettings, currentSelectedIt } return { messageItem, contentDiv, avatarImg, senderNameDiv, nameTimeDiv, detailsAndBubbleWrapper }; -} - -// Expose to global scope for classic scripts -window.domBuilder = { - createMessageSkeleton, - formatMessageTimestamp, - applyUserMessageLayoutState -}; +} diff --git a/modules/renderer/domListenerOwner.js b/modules/renderer/domListenerOwner.js new file mode 100644 index 000000000..858e7e52e --- /dev/null +++ b/modules/renderer/domListenerOwner.js @@ -0,0 +1,46 @@ +/** Owns DOM event registrations for one renderer lifecycle and removes them on dispose. */ +export function createDomListenerOwner({ reportError = console.error } = {}) { + const registrations = []; + let disposed = false; + let capturingOwned = false; + const add = (target, type, handler, options) => { + if (disposed || !target?.addEventListener || typeof handler !== 'function') return false; + capturingOwned = true; + try { target.addEventListener(type, handler, options); } finally { capturingOwned = false; } + registrations.push(() => target.removeEventListener?.(type, handler, options)); + return true; + }; + const own = resource => { + if (disposed || !resource) return resource; + const dispose = typeof resource === 'function' ? resource : (resource.disconnect || resource.dispose || resource.abort); + if (typeof dispose === 'function') registrations.push(() => dispose.call(resource)); + return resource; + }; + const timeout = (callback, delay) => { + if (disposed || typeof callback !== 'function') return null; + const handle = setTimeout(() => { if (!disposed) callback(); }, delay); + registrations.push(() => clearTimeout(handle)); + return handle; + }; + const capture = () => { + if (disposed || typeof EventTarget === 'undefined') return () => {}; + const originalAdd = EventTarget.prototype.addEventListener; + const originalRemove = EventTarget.prototype.removeEventListener; + const owner = EventTarget.prototype; + owner.addEventListener = function(type, handler, options) { + if (!capturingOwned && typeof handler === 'function') { + registrations.push(() => originalRemove.call(this, type, handler, options)); + } + return originalAdd.call(this, type, handler, options); + }; + return () => { owner.addEventListener = originalAdd; }; + }; + const dispose = () => { + if (disposed) return; + disposed = true; + for (const remove of registrations.splice(0).reverse()) { + try { remove(); } catch (error) { reportError('[DomListenerOwner] remove failed', error); } + } + }; + return Object.freeze({ add, own, timeout, capture, dispose }); +} diff --git a/modules/renderer/emoticonUrlFixer.js b/modules/renderer/emoticonUrlFixer.js index fa22e48db..856daeee7 100644 --- a/modules/renderer/emoticonUrlFixer.js +++ b/modules/renderer/emoticonUrlFixer.js @@ -1,6 +1,8 @@ -// modules/renderer/emoticonUrlFixer.js - -let emoticonLibrary = []; +// modules/renderer/emoticonUrlFixer.js + +/** Creates one emoticon catalog owner for one renderer or viewer. */ +export function createEmoticonUrlFixer() { +let emoticonLibrary = []; let isInitialized = false; let electronAPI; let initializationPromise = null; @@ -192,4 +194,5 @@ function fixEmoticonUrl(originalSrc) { return originalSrc; } -export { initialize, fixEmoticonUrl }; + return Object.freeze({ initialize, fixEmoticonUrl }); +} diff --git a/modules/renderer/enhancedColorUtils.js b/modules/renderer/enhancedColorUtils.js index 90cb1001b..ad4b4312c 100644 --- a/modules/renderer/enhancedColorUtils.js +++ b/modules/renderer/enhancedColorUtils.js @@ -83,10 +83,8 @@ class TTLCache { // Enhanced avatar color cache with TTL export const avatarColorCache = new TTLCache(24 * 60 * 60 * 1000); // 24 hours -// Start periodic cleanup -setInterval(() => { - avatarColorCache.cleanup(); -}, 60 * 60 * 1000); // Clean up every hour +// Expired entries are removed on access. A module-level interval would keep a +// renderer process alive after its Surface owner has been disposed. // Enhanced color extraction with better error handling export function getDominantAvatarColor(imageUrl) { @@ -190,4 +188,4 @@ export function getDominantAvatarColor(imageUrl) { } // Export cache for debugging -export { TTLCache }; \ No newline at end of file +export { TTLCache }; diff --git a/modules/renderer/forwardMessageOwner.js b/modules/renderer/forwardMessageOwner.js new file mode 100644 index 000000000..95327c17b --- /dev/null +++ b/modules/renderer/forwardMessageOwner.js @@ -0,0 +1,41 @@ +/** Owns the forward-message modal state, target selection and send operation. */ +export function createForwardMessageOwner({ chatAPI, chatManager, uiHelperFunctions, getConversation, documentRef = document, reportError = console.error } = {}) { + let message = null; + let target = null; + let disposed = false; + const renderTargets = items => { + const list = documentRef.getElementById('forwardTargetList'); + const confirm = documentRef.getElementById('confirmForwardBtn'); + if (!list) return; + list.innerHTML = ''; + for (const item of items || []) { + const li = documentRef.createElement('li'); li.className = 'agent-item'; li.dataset.id = item.id; li.dataset.type = item.type; li.dataset.name = item.name; + const avatar = documentRef.createElement('img'); avatar.className = 'avatar'; avatar.src = item.avatarUrl || (item.type === 'group' ? 'assets/default_group_avatar.png' : 'assets/default_user_avatar.png'); + const name = documentRef.createElement('span'); name.className = 'agent-name'; name.textContent = `${item.name} (${item.type === 'group' ? '群组' : 'Agent'})`; + li.append(avatar, name); li.onclick = () => { list.querySelector('.selected')?.classList.remove('selected'); li.classList.add('selected'); target = { id: item.id, type: item.type, name: item.name }; if (confirm) confirm.disabled = false; }; list.appendChild(li); + } + }; + const show = async incoming => { + if (disposed) return; + message = incoming; target = null; uiHelperFunctions?.openModal?.('forwardMessageModal'); + const list = documentRef.getElementById('forwardTargetList'); const search = documentRef.getElementById('forwardTargetSearch'); const comment = documentRef.getElementById('forwardAdditionalComment'); const confirm = documentRef.getElementById('confirmForwardBtn'); + if (!list || !search || !comment || !confirm) { reportError('[ForwardMessageOwner] modal elements missing'); return; } + list.innerHTML = '
  • Loading...
  • '; comment.value = ''; search.value = ''; confirm.disabled = true; + const result = await chatAPI?.getAllItems?.(); renderTargets(result?.success ? result.items : []); + search.oninput = () => { const term = search.value.toLowerCase(); list.querySelectorAll('.agent-item').forEach(item => { item.style.display = item.dataset.name.toLowerCase().includes(term) ? '' : 'none'; }); }; + confirm.onclick = confirmForward; + }; + const confirmForward = async () => { + if (disposed || !message || !target) { uiHelperFunctions?.showToastNotification?.('错误:未选择消息或转发目标。', 'error'); return; } + const comment = documentRef.getElementById('forwardAdditionalComment')?.value?.trim() || ''; + const conversation = getConversation?.() || {}; + const result = await chatAPI?.getOriginalMessageContent?.(conversation.item?.id, conversation.item?.type, conversation.topicId, message.id); + if (!result?.success) { uiHelperFunctions?.showToastNotification?.(`无法获取原始消息内容: ${result?.error || 'unknown error'}`, 'error'); return; } + const original = { ...message, content: result.content }; const sender = original.name || (original.role === 'user' ? '用户' : '助手'); let content = `> 转发自 **${sender}** 的消息:\n\n`; + content += typeof original.content === 'string' ? original.content : (original.content?.text || ''); if (comment) content += `\n\n---\n${comment}`; + if (typeof chatManager?.handleForwardMessage !== 'function') { uiHelperFunctions?.showToastNotification?.('转发功能尚未完全实现。', 'error'); return; } + await chatManager.handleForwardMessage(target, content, original.attachments || []); uiHelperFunctions?.showToastNotification?.(`消息已转发给 ${target.name}`, 'success'); uiHelperFunctions?.closeModal?.('forwardMessageModal'); message = null; target = null; + }; + const dispose = async () => { disposed = true; message = null; target = null; }; + return Object.freeze({ show, confirm: confirmForward, dispose }); +} diff --git a/modules/renderer/imageHandler.js b/modules/renderer/imageHandler.js index 15d290c40..b242b397b 100644 --- a/modules/renderer/imageHandler.js +++ b/modules/renderer/imageHandler.js @@ -1,19 +1,26 @@ // modules/renderer/imageHandler.js -import { fixEmoticonUrl } from './emoticonUrlFixer.js'; - - -let imageHandlerRefs = { - electronAPI: null, - uiHelper: null, - chatMessagesDiv: null, -}; - -export function initializeImageHandler(refs) { - imageHandlerRefs.electronAPI = refs.electronAPI; - imageHandlerRefs.uiHelper = refs.uiHelper; - imageHandlerRefs.chatMessagesDiv = refs.chatMessagesDiv; - console.log("[ImageHandler] Initialized."); -} +/** Creates one image interaction owner for one MessageRenderer instance. */ +export function createImageHandler({ fixUrl = value => value } = {}) { + let imageHandlerRefs = null; + const ownedContentListeners = new Map(); + + function cleanupContent(contentDiv) { + const disposers = ownedContentListeners.get(contentDiv); + if (!disposers) return; + ownedContentListeners.delete(contentDiv); + disposers.splice(0).reverse().forEach(dispose => dispose()); + } + + function initialize(refs) { + if (!refs?.electronAPI || !refs?.chatMessagesDiv) { + throw new TypeError('ImageHandler requires Electron transport and a Surface root'); + } + imageHandlerRefs = Object.freeze({ + electronAPI: refs.electronAPI, + uiHelper: refs.uiHelper || null, + chatMessagesDiv: refs.chatMessagesDiv, + }); + } /** * 将内容设置到DOM元素,并处理其中的图片。 @@ -22,9 +29,13 @@ export function initializeImageHandler(refs) { * @param {string} rawHtml - 经过marked.parse()处理的原始HTML。 * @param {string} messageId - 消息ID。 */ -export function setContentAndProcessImages(contentDiv, rawHtml, messageId) { - // 🟢 直接设置 HTML,不做替换 - contentDiv.innerHTML = rawHtml; + function setContentAndProcessImages(contentDiv, rawHtml, messageId) { + if (!imageHandlerRefs) throw new Error('ImageHandler is not initialized'); + cleanupContent(contentDiv); + // 🟢 直接设置 HTML,不做替换 + contentDiv.innerHTML = rawHtml; + const transport = imageHandlerRefs.electronAPI; + const listenerDisposers = []; // 🟢 然后对所有 添加事件监听 const images = contentDiv.querySelectorAll('img'); @@ -32,8 +43,8 @@ export function setContentAndProcessImages(contentDiv, rawHtml, messageId) { let src = img.src; // 修复表情包 URL - if (fixEmoticonUrl && src.includes('表情包')) { - const fixedSrc = fixEmoticonUrl(src); + if (fixUrl && src.includes('表情包')) { + const fixedSrc = fixUrl(src); if (fixedSrc !== src) { img.src = fixedSrc; src = fixedSrc; @@ -44,20 +55,33 @@ export function setContentAndProcessImages(contentDiv, rawHtml, messageId) { img.style.cursor = 'pointer'; img.title = `点击在新窗口预览\n右键可复制图片`; - img.addEventListener('click', (e) => { - e.stopPropagation(); - const currentTheme = document.body.classList.contains('light-theme') ? 'light' : 'dark'; - imageHandlerRefs.electronAPI.openImageViewer({ - src: src, - title: img.alt || src.split('/').pop() || 'AI 图片', - theme: currentTheme - }); - }); - - img.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - imageHandlerRefs.electronAPI.showImageContextMenu(src); - }); - }); -} \ No newline at end of file + const onClick = (e) => { + e.stopPropagation(); + const currentTheme = contentDiv.ownerDocument.body.classList.contains('light-theme') ? 'light' : 'dark'; + transport.openImageViewer({ + src: src, + title: img.alt || src.split('/').pop() || 'AI 图片', + theme: currentTheme + }); + }; + + const onContextMenu = (e) => { + e.preventDefault(); + e.stopPropagation(); + transport.showImageContextMenu(src); + }; + img.addEventListener('click', onClick); + img.addEventListener('contextmenu', onContextMenu); + listenerDisposers.push(() => img.removeEventListener('click', onClick)); + listenerDisposers.push(() => img.removeEventListener('contextmenu', onContextMenu)); + }); + if (listenerDisposers.length > 0) ownedContentListeners.set(contentDiv, listenerDisposers); +} + + function dispose() { + [...ownedContentListeners.keys()].forEach(cleanupContent); + imageHandlerRefs = null; + } + + return Object.freeze({ initialize, setContentAndProcessImages, cleanupContent, dispose }); +} diff --git a/modules/renderer/mainChatAttachmentOwner.js b/modules/renderer/mainChatAttachmentOwner.js new file mode 100644 index 000000000..efccc21e8 --- /dev/null +++ b/modules/renderer/mainChatAttachmentOwner.js @@ -0,0 +1,42 @@ +/** Owns the main window's pending attachment state and preview projection. */ +export function createMainChatAttachmentOwner({ renderPreview = null } = {}) { + let files = Object.freeze([]); + let disposed = false; + + const assertActive = () => { + if (disposed) throw new Error('MainChatAttachmentOwner is disposed.'); + }; + const ref = Object.freeze({ + get: () => files, + set(value) { + assertActive(); + files = Object.freeze(Array.isArray(value) ? [...value] : []); + return files; + }, + append(value) { + assertActive(); + files = Object.freeze([...files, value]); + return files; + }, + }); + + return Object.freeze({ + ref, + get: ref.get, + clear() { + assertActive(); + files = Object.freeze([]); + return files; + }, + syncPreview() { + if (disposed) return false; + renderPreview?.(files); + return true; + }, + dispose() { + if (disposed) return; + disposed = true; + files = Object.freeze([]); + }, + }); +} diff --git a/modules/renderer/mainChatAuxiliaryEventOwner.js b/modules/renderer/mainChatAuxiliaryEventOwner.js new file mode 100644 index 000000000..c57448785 --- /dev/null +++ b/modules/renderer/mainChatAuxiliaryEventOwner.js @@ -0,0 +1,40 @@ +import { createOwnedPreloadSubscription } from './ownedPreloadSubscription.js'; + +/** Owns non-stream preload events that project into the main chat Surface. */ +export function createMainChatAuxiliaryEventOwner({ + subscriptions = {}, + insertSharedText, + consumeLogStatus, + consumeLogMessage, + consumeGroupTopicUpdate, +} = {}) { + const receipts = []; + let mounted = false; + let disposed = false; + + const add = (subscribe, consume) => { + if (typeof subscribe !== 'function' || typeof consume !== 'function') return; + receipts.push(createOwnedPreloadSubscription({ + subscribe, + consume: (payload, lifecycle) => consume(payload, lifecycle), + })); + }; + + const mount = () => { + if (disposed) throw new Error('MainChatAuxiliaryEventOwner is disposed'); + if (mounted) return; + mounted = true; + add(subscriptions.loomShareText, insertSharedText); + add(subscriptions.logStatus, consumeLogStatus); + add(subscriptions.logMessage, consumeLogMessage); + add(subscriptions.groupTopicUpdated, consumeGroupTopicUpdate); + }; + + const dispose = async () => { + if (disposed) return; + disposed = true; + await Promise.allSettled(receipts.splice(0).map(receipt => receipt.dispose())); + }; + + return Object.freeze({ mount, dispose }); +} diff --git a/modules/renderer/mainChatComposition.js b/modules/renderer/mainChatComposition.js new file mode 100644 index 000000000..d8e4e256f --- /dev/null +++ b/modules/renderer/mainChatComposition.js @@ -0,0 +1,87 @@ +import { createChatOperations } from '../chat/chatOperation.js'; +import { createMainChatSurfaceAdapter } from './mainChatSurfaceAdapter.js'; + +/** + * Owns the main chat Surface composition. This module deliberately receives + * already-created providers; it does not discover renderer globals or bind + * application services itself. + */ +export function createMainChatComposition({ + root, + messageInput, + messageRenderer, + streamProjection, + chatRepository, + historyPersistence, + presentationState, + renderDependencies, + chatManager, + flowlockManager, + currentSelection, + currentTopicId, + chatWindow, + dispatchTerminal, + notifySendStateChanged, + interrupt, + showForwardModal, + provideCapabilities, + capabilitySnapshot, + settings, + createInternalRenderer, + disposeCapabilities, +}) { + const adapter = createMainChatSurfaceAdapter({ + root, + renderer: messageRenderer, + repository: chatRepository, + focusTarget: messageInput, + operations: createChatOperations({ + send: request => chatManager?.sendMessage?.(request), + cancel: () => interrupt(), + }), + presentationState, + renderDependencies, + streamServices: { + streamProjection, + historyPersistence, + messageRenderer, + chatManager, + flowlockManager, + getSelection: currentSelection, + getTopicId: currentTopicId, + dispatchTerminal, + notifySendStateChanged, + }, + disposeRenderer: async () => { + await messageRenderer.disposeRootResources(root); + messageRenderer.disposeRendererResources(); + await streamProjection?.dispose?.(); + }, + ownerWindow: chatWindow, + onDispose: async () => { + await disposeCapabilities?.(); + }, + }); + + const release = provideCapabilities?.({ + repository: chatRepository, + getSnapshot: capabilitySnapshot, + createRenderer: createInternalRenderer, + manager: chatManager, + presentation: presentationState, + settings, + }) || null; + + messageRenderer.setContextMenuDependencies({ + showForwardModal, + acceptStreamEvent: event => adapter.acceptStreamEvent(event), + cancelStream: (messageId, reason) => adapter.cancelStream(messageId, reason), + }); + + return Object.freeze({ + adapter, + surface: adapter.surface, + domRenderer: adapter.domRenderer, + releaseCapabilities: release, + }); +} diff --git a/modules/renderer/mainChatDomBindings.js b/modules/renderer/mainChatDomBindings.js new file mode 100644 index 000000000..e4f9093cc --- /dev/null +++ b/modules/renderer/mainChatDomBindings.js @@ -0,0 +1,69 @@ +const byId = (document, id) => document.getElementById(id); + +/** + * Resolves the fixed main-window DOM contract once at the composition edge. + * Feature modules receive these nodes explicitly; they must not rediscover + * the main document through ambient globals. + */ +export function createMainChatDomBindings(document) { + if (!document?.getElementById || !document?.querySelector) { + throw new TypeError('MainChatDomBindings requires an owning document'); + } + + const bindings = { + itemListUl: byId(document, 'agentList'), + currentChatNameH3: byId(document, 'currentChatAgentName'), + chatMessagesDiv: byId(document, 'chatMessages'), + messageInput: byId(document, 'messageInput'), + sendMessageBtn: byId(document, 'sendMessageBtn'), + attachFileBtn: byId(document, 'attachFileBtn'), + emoticonTriggerBtn: byId(document, 'emoticonTriggerBtn'), + quickNewTopicBtn: byId(document, 'quickNewTopicBtn'), + attachmentPreviewArea: byId(document, 'attachmentPreviewArea'), + chatInputCard: document.querySelector('.chat-input-card'), + globalSettingsBtn: byId(document, 'globalSettingsBtn'), + itemSettingsContainerTitle: byId(document, 'agentSettingsContainerTitle'), + selectedItemNameForSettingsSpan: byId(document, 'selectedAgentNameForSettings'), + agentSettingsContainer: byId(document, 'agentSettingsContainer'), + agentSettingsForm: byId(document, 'agentSettingsForm'), + editingAgentIdInput: byId(document, 'editingAgentId'), + agentNameInput: byId(document, 'agentNameInput'), + agentAvatarInput: byId(document, 'agentAvatarInput'), + agentAvatarPreview: byId(document, 'agentAvatarPreview'), + agentSystemPromptTextarea: byId(document, 'agentSystemPrompt'), + agentModelInput: byId(document, 'agentModel'), + agentTemperatureInput: byId(document, 'agentTemperature'), + agentContextTokenLimitInput: byId(document, 'agentContextTokenLimit'), + agentMaxOutputTokensInput: byId(document, 'agentMaxOutputTokens'), + groupSettingsContainer: byId(document, 'groupSettingsContainer'), + selectItemPromptForSettings: byId(document, 'selectAgentPromptForSettings'), + deleteItemBtn: byId(document, 'deleteAgentBtn'), + currentItemActionBtn: byId(document, 'currentAgentSettingsBtn'), + clearCurrentChatBtn: byId(document, 'clearCurrentChatBtn'), + toggleNotificationsBtn: byId(document, 'toggleNotificationsBtn'), + notificationsSidebar: byId(document, 'notificationsSidebar'), + vcpLogConnectionStatusDiv: byId(document, 'vcpLogConnectionStatus'), + notificationsListUl: byId(document, 'notificationsList'), + sidebarTabButtons: document.querySelectorAll('.sidebar-tab-button'), + sidebarTabContents: document.querySelectorAll('.sidebar-tab-content'), + tabContentTopics: byId(document, 'tabContentTopics'), + tabContentSettings: byId(document, 'tabContentSettings'), + topicSearchInput: byId(document, 'topicSearchInput'), + leftSidebar: document.querySelector('.sidebar'), + rightNotificationsSidebar: byId(document, 'notificationsSidebar'), + resizerLeft: byId(document, 'resizerLeft'), + resizerRight: byId(document, 'resizerRight'), + agentSearchInput: byId(document, 'agentSearchInput'), + notificationTitleElement: byId(document, 'notificationTitle'), + digitalClockElement: byId(document, 'digitalClock'), + dateDisplayElement: byId(document, 'dateDisplay'), + toggleAssistantBtn: byId(document, 'toggleAssistantBtn'), + toggleSidebarModeBtn: byId(document, 'toggleSidebarModeBtn'), + openModelSelectBtn: byId(document, 'openModelSelectBtn'), + }; + + for (const required of ['itemListUl', 'chatMessagesDiv', 'messageInput', 'sendMessageBtn']) { + if (!bindings[required]) throw new Error(`MainChatDomBindings missing required node: ${required}`); + } + return Object.freeze(bindings); +} diff --git a/modules/renderer/mainChatEventBridge.js b/modules/renderer/mainChatEventBridge.js new file mode 100644 index 000000000..c3a3b1008 --- /dev/null +++ b/modules/renderer/mainChatEventBridge.js @@ -0,0 +1,39 @@ +/** + * Owns the main-window VCP event subscription. The preload subscription is + * the producer; this bridge is the single consumer router for the main + * Surface and returns the producer's disposer to its composition owner. + */ +export function createMainChatEventBridge({ + chatAPI, + acceptStreamEvent, + consumeNonStreamingEvent, + onUnhandled = console.warn, +} = {}) { + if (!chatAPI || typeof chatAPI.onVCPStreamEvent !== 'function') { + throw new TypeError('Main chat event bridge requires onVCPStreamEvent'); + } + if (typeof acceptStreamEvent !== 'function') { + throw new TypeError('Main chat event bridge requires an owned stream consumer'); + } + let disposed = false; + const subscription = chatAPI.onVCPStreamEvent(async eventData => { + if (disposed) return false; + const messageId = String(eventData?.messageId || '').trim(); + if (!messageId) { + onUnhandled('[MainChatEventBridge] Ignoring stream event without messageId', eventData); + return false; + } + if (acceptStreamEvent(eventData)) return true; + if (await consumeNonStreamingEvent?.(eventData)) return true; + if (eventData?.type === 'no_ai_response') return true; + onUnhandled('[MainChatEventBridge] Unhandled stream event', eventData); + return false; + }); + return Object.freeze({ + dispose() { + disposed = true; + if (typeof subscription === 'function') subscription(); + else subscription?.dispose?.(); + }, + }); +} diff --git a/modules/renderer/mainChatFlowlockOwner.js b/modules/renderer/mainChatFlowlockOwner.js new file mode 100644 index 000000000..d8d0fc046 --- /dev/null +++ b/modules/renderer/mainChatFlowlockOwner.js @@ -0,0 +1,35 @@ +import { createOwnedPreloadSubscription } from './ownedPreloadSubscription.js'; + +/** Owns plugin Flowlock commands and their request/response lifecycle for the main chat Surface. */ +export function createMainChatFlowlockOwner({ subscriptions = {}, flowlockManager, getSelectedItem, messageInput, uiHelperFunctions, sendResponse, reportError = console.error } = {}) { + const receipts = []; let mounted = false; let disposed = false; + const respond = (data, result, lifecycle = null) => { + if (disposed || lifecycle?.isActive?.() === false || !data?.requestId || typeof sendResponse !== 'function') return; + try { sendResponse(data, result); } catch (error) { reportError('[MainChatFlowlockOwner] response failed', error); } + }; + const resize = () => uiHelperFunctions?.autoResizeTextarea?.(messageInput); + const handle = async (data = {}, lifecycle = null) => { + if (disposed || lifecycle?.isActive?.() === false) return; + const { command, agentId, topicId, prompt, promptSource, target, oldText, newText } = data; + const targetAgentId = agentId || getSelectedItem?.()?.id; + try { + if (!flowlockManager) { respond(data, { command, success: false, error: 'flowlockManager not available' }, lifecycle); return; } + switch (command) { + case 'start': if (!targetAgentId || !topicId) throw new Error('Missing agentId or topicId for start command'); await flowlockManager.start(targetAgentId, topicId, { startImmediately: false }); if (disposed || lifecycle?.isActive?.() === false) return; break; + case 'stop': if (!targetAgentId) throw new Error('Missing agentId for stop command'); await flowlockManager.stop(targetAgentId); if (disposed || lifecycle?.isActive?.() === false) return; break; + case 'promptee': if (!targetAgentId || !prompt) throw new Error('Missing target agent or prompt for promptee command'); flowlockManager.setCustomPrompt(targetAgentId, prompt); break; + case 'prompter': if (!promptSource) throw new Error('Missing promptSource for prompter command'); if (messageInput) { const value = messageInput.value || ''; messageInput.value = value + (value ? ' ' : '') + `[来自: ${promptSource}]`; resize(); } break; + case 'clear': if (messageInput) { messageInput.value = ''; resize(); } break; + case 'remove': if (!target) throw new Error('Missing target for remove command'); if (messageInput) { messageInput.value = (messageInput.value || '').split(target).join(''); resize(); } break; + case 'edit': if (!oldText || newText === undefined) throw new Error('Missing oldText or newText for edit command'); if (messageInput) { const value = messageInput.value || ''; const index = value.indexOf(oldText); if (index !== -1) messageInput.value = value.slice(0, index) + newText + value.slice(index + oldText.length); resize(); } break; + case 'get': if (!messageInput) throw new Error('Message input element not found'); respond(data, { command: 'get', success: true, content: messageInput.value || '' }, lifecycle); return; + case 'status': { const state = targetAgentId ? flowlockManager.getSession?.(targetAgentId) : null; respond(data, { command: 'status', success: true, status: { isActive: state?.status === 'active', isProcessing: !!state?.activeMessageId, agentId: state?.agentId || targetAgentId || null, topicId: state?.topicId || null, session: state, activeAgents: flowlockManager.getActiveAgents?.() || [] } }, lifecycle); return; } + default: throw new Error(`Unknown flowlock command: ${command}`); + } + respond(data, { command, success: true }, lifecycle); + } catch (error) { reportError('[MainChatFlowlockOwner] command failed', error); respond(data, { command, success: false, error: error?.message || String(error) }, lifecycle); } + }; + const mount = () => { if (disposed) throw new Error('MainChatFlowlockOwner is disposed'); if (mounted) return; mounted = true; for (const subscribe of [subscriptions.command, subscriptions.request]) if (typeof subscribe === 'function') receipts.push(createOwnedPreloadSubscription({ subscribe, consume: handle, reportError })); }; + const dispose = async () => { if (disposed) return; disposed = true; await Promise.allSettled(receipts.splice(0).map(receipt => receipt.dispose())); }; + return Object.freeze({ mount, dispose, handle }); +} diff --git a/modules/renderer/mainChatSendOwner.js b/modules/renderer/mainChatSendOwner.js new file mode 100644 index 000000000..fd74826ec --- /dev/null +++ b/modules/renderer/mainChatSendOwner.js @@ -0,0 +1,129 @@ +const INTERRUPT_BUTTON_HTML = ` + +`; + +/** Owns main-chat send/interrupt policy and its button projection. */ +export function createMainChatSendOwner({ + button, + messagesRoot, + historyRef, + selectedItemRef, + topicIdRef, + streamProjection, + chatAPI, + interruptHandler, + getAdapter, + getChatManager, + messageRenderer, + notify, +}) { + const defaultButtonHtml = button?.innerHTML || ''; + let disposed = false; + + const isContextCurrent = context => { + const selected = selectedItemRef.get(); + const topicId = topicIdRef.get(); + if (!context || !selected?.id || !topicId) return false; + return (context.groupId || context.agentId) === selected.id && context.topicId === topicId; + }; + + const getInterruptibleMessage = () => { + const history = historyRef.get(); + for (let index = history.length - 1; index >= 0; index--) { + const message = history[index]; + if (!message || message.role !== 'assistant') continue; + const item = messagesRoot?.querySelector(`.message-item[data-message-id="${message.id}"]`); + const isStreaming = Boolean(item?.classList.contains('streaming')); + if ((item?.isConnected && message.isThinking === true) || isStreaming) { + return { ...message, isStreaming }; + } + } + + const messageId = streamProjection?.getActiveStreamingMessageId?.(); + const context = streamProjection?.getActiveStreamingContext?.(); + if (!messageId || !isContextCurrent(context)) return null; + const existing = history.find(message => message?.id === messageId && message.role === 'assistant'); + if (existing) return { ...existing, isStreaming: true }; + const selected = selectedItemRef.get(); + return { + id: messageId, + role: 'assistant', + name: context.agentName || selected?.name || selected?.id, + agentId: context.agentId, + groupId: context.groupId, + isGroupMessage: context.isGroupMessage === true, + avatarUrl: context.avatarUrl || selected?.avatarUrl, + avatarColor: context.avatarColor || selected?.config?.avatarCalculatedColor, + isStreaming: true, + }; + }; + + const update = () => { + if (disposed || !button) return false; + const mode = getInterruptibleMessage() ? 'interrupt' : 'send'; + button.dataset.mode = mode; + button.classList.toggle('interrupt-mode', mode === 'interrupt'); + // The send control is the main-chat composer status surface. Keep its + // busy state synchronized with the same interruptible-stream decision + // used for the command projection so assistive technology observes + // the operation without relying on CSS classes or button labels. + button.setAttribute('aria-busy', String(mode === 'interrupt')); + button.innerHTML = mode === 'interrupt' ? INTERRUPT_BUTTON_HTML : defaultButtonHtml; + button.title = mode === 'interrupt' ? '中止回复' : '发送消息/右键高级回复'; + button.setAttribute('aria-label', mode === 'interrupt' ? '中止回复' : '发送消息'); + return true; + }; + + const interrupt = async () => { + const activeMessage = getInterruptibleMessage(); + if (!activeMessage || disposed) return false; + const selected = selectedItemRef.get(); + const isGroup = activeMessage.isGroupMessage === true || selected?.type === 'group'; + let result = { success: false, error: '无法发送中止请求。' }; + if (isGroup) { + result = typeof chatAPI?.interruptGroupRequest === 'function' + ? await chatAPI.interruptGroupRequest(activeMessage.id) + : { success: false, error: '群聊中止接口不可用。' }; + } else if (typeof interruptHandler?.interrupt === 'function') { + result = await interruptHandler.interrupt(activeMessage.id); + } + if (disposed) return false; + if (result.success) { + notify?.('已发送中止信号。', 'success'); + return true; + } + + const localOutcome = await getAdapter()?.cancelStream?.( + activeMessage.id, + result.error || 'interrupt-request-failed' + ); + if (disposed) return false; + if (!localOutcome) { + streamProjection?.discardStreamingMessage?.(activeMessage.id); + historyRef.set(historyRef.get().filter(message => message?.id !== activeMessage.id)); + messageRenderer?.removeMessageById?.(activeMessage.id, false); + } + update(); + notify?.(`中止失败:${result.error || '未知错误'},已在本地停止。`, 'error'); + return true; + }; + + return Object.freeze({ + getInterruptibleMessage, + update, + interrupt, + async handleAction() { + if (disposed) return false; + if (getInterruptibleMessage()) return interrupt(); + await getChatManager()?.handleSendMessage?.(); + return true; + }, + dispose() { + if (disposed) return; + disposed = true; + }, + }); +} diff --git a/modules/renderer/mainChatSettingsOwner.js b/modules/renderer/mainChatSettingsOwner.js new file mode 100644 index 000000000..e76a188b4 --- /dev/null +++ b/modules/renderer/mainChatSettingsOwner.js @@ -0,0 +1,33 @@ +/** Owns main-window settings state and exposes a compatibility-safe reference for legacy consumers. */ +export function createMainChatSettingsOwner({ initial = {}, publish = () => {} } = {}) { + const clone = value => { + if (!value || typeof value !== 'object') return value; + if (Array.isArray(value)) return value.map(clone); + return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, clone(nested)])); + }; + const freeze = value => { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; + Object.values(value).forEach(freeze); + return Object.freeze(value); + }; + const settings = clone(initial); + let disposed = false; + const get = () => clone(settings); + const snapshot = () => freeze(clone(settings)); + const replace = next => { + if (disposed) return snapshot(); + Object.assign(settings, clone(next || {})); + publish(snapshot()); + return snapshot(); + }; + const update = (key, value) => replace({ [key]: value }); + const dispose = () => { disposed = true; }; + return Object.freeze({ + get, + snapshot, + replace, + update, + dispose, + ref: Object.freeze({ get, set: value => replace(value) }), + }); +} diff --git a/modules/renderer/mainChatSettingsPresentationOwner.js b/modules/renderer/mainChatSettingsPresentationOwner.js new file mode 100644 index 000000000..551896e86 --- /dev/null +++ b/modules/renderer/mainChatSettingsPresentationOwner.js @@ -0,0 +1,917 @@ +/** Owns global-settings DOM projection, chat layout controls and presentation bindings. */ +export function createMainChatSettingsPresentationOwner({ + documentRef, + settingsOwner, + listenerOwner, + chatAPI, + loadSettings, + startupThemeGate, + startupTimeoutMs = 15_000, + elements = {}, + notificationRenderer = null, + messageRenderer = null, + windowRef = documentRef?.defaultView, + getSettingsManager = () => null, + getAppearance = () => null, + getPretextBridge = () => null, +} = {}) { + if (!documentRef || !settingsOwner || !listenerOwner) { + throw new TypeError('MainChatSettingsPresentationOwner requires document, settings and listener owners.'); + } + const document = documentRef; + let globalSettings = settingsOwner.get(); + let themeOwner = null; + let loadSettingsCapability = loadSettings; + let startupThemeGateCapability = startupThemeGate; + let disposed = false; + let generation = 0; + const tasks = new Set(); + const isCurrent = token => !disposed && token === generation; + const track = value => { + const task = Promise.resolve(value); + tasks.add(task); + task.finally(() => tasks.delete(task)).catch(() => {}); + return task; + }; + + async function loadAndApply() { + const token = generation; + if (!isCurrent(token)) return false; + let settings; + try { + settings = await loadSettingsCapability(chatAPI?.loadSettings, startupTimeoutMs, '加载设置超时'); + if (!isCurrent(token)) return false; + } catch (error) { + startupThemeGateCapability.release({ mode: 'system', message: error?.message || '设置加载失败' }); + throw error; + } + if (!settings || settings.error) { + console.warn('加载全局设置失败或无设置:', settings?.error); + startupThemeGateCapability.release({ mode: 'system', message: settings?.error || '设置加载失败,已使用系统主题' }); + notificationRenderer?.updateVCPLogStatus?.( + { status: 'error', message: 'VCPLog未配置' }, + elements.vcpLogConnectionStatus + ); + return false; + } + + settingsOwner.replace(settings); + globalSettings = settingsOwner.get(); + startupThemeGateCapability.release({ mode: globalSettings.currentThemeMode }); + const appearanceProfile = getAppearance()?.commit( + globalSettings.appearanceProfile, + { uiMode: 'next', source: 'settings-load' } + ) || globalSettings.appearanceProfile; + const filterEnabled = globalSettings.filterEnabled + ?? globalSettings.doNotDisturbLogMode + ?? (windowRef?.localStorage?.getItem('doNotDisturbLogMode') === 'true'); + settingsOwner.replace({ appearanceProfile, filterEnabled }); + globalSettings = settingsOwner.get(); + windowRef?.dispatchEvent(new windowRef.CustomEvent('global-settings-updated', { + detail: { settings: globalSettings, source: 'settings-load' } + })); + applyChatBubbleLayoutSettings(globalSettings); + + if (globalSettings.sidebarWidth && elements.leftSidebar) { + elements.leftSidebar.style.width = `${globalSettings.sidebarWidth}px`; + } + if (elements.leftSidebar) { + const sidebarIsActive = globalSettings.sidebarActive !== false; + const avatarOnly = sidebarIsActive && globalSettings.sidebarAvatarOnly === true; + elements.leftSidebar.classList.toggle('active', sidebarIsActive); + elements.leftSidebar.classList.toggle('avatar-only', avatarOnly); + document.querySelector('.main-content')?.classList.toggle('sidebar-active', sidebarIsActive); + } + if (globalSettings.notificationsSidebarWidth && elements.rightNotificationsSidebar?.classList.contains('active')) { + elements.rightNotificationsSidebar.style.width = `${globalSettings.notificationsSidebarWidth}px`; + } + + if (globalSettings.vcpLogUrl && globalSettings.vcpLogKey) { + notificationRenderer?.updateVCPLogStatus?.( + { status: 'connecting', message: '连接中...' }, + elements.vcpLogConnectionStatus + ); + chatAPI.connectVCPLog(globalSettings.vcpLogUrl, globalSettings.vcpLogKey); + } else { + notificationRenderer?.updateVCPLogStatus?.( + { status: 'error', message: 'VCPLog未配置' }, + elements.vcpLogConnectionStatus + ); + } + + elements.toggleAssistant?.classList.toggle('active', Boolean(globalSettings.assistantEnabled)); + if (elements.toggleSidebarMode) { + const avatarOnly = globalSettings.sidebarActive !== false && globalSettings.sidebarAvatarOnly === true; + elements.toggleSidebarMode.classList.toggle('active', avatarOnly); + elements.toggleSidebarMode.setAttribute('aria-pressed', String(avatarOnly)); + } + listenerOwner.add(document, 'modal-ready', event => { + if (event.detail?.modalId === 'globalSettingsModal' && !disposed) track(syncGlobalSettingsToUI()); + }); + messageRenderer?.setUserAvatar?.(globalSettings.userAvatarUrl); + messageRenderer?.setUserAvatarColor?.(globalSettings.userAvatarCalculatedColor); + return true; + } + + const CHAT_PRESENTATION_MODES = Object.freeze(['bubble', 'panel', 'immersive']); + const CHAT_PRESENTATION_MODE_CLASSES = CHAT_PRESENTATION_MODES.map( + mode => `chat-presentation-${mode}` + ); + + function normalizeChatPresentationMode(mode) { return themeOwner.normalizePresentation(mode); } + + + function isChatVisuallyNearBottom(scrollContainer, threshold = 64) { + const messages = scrollContainer?.querySelector('.chat-messages'); + if (!scrollContainer || !messages) return true; + + const containerRect = scrollContainer.getBoundingClientRect(); + const messagesRect = messages.getBoundingClientRect(); + return Math.abs(messagesRect.bottom - containerRect.bottom) <= threshold; + } + + function captureChatPresentationScrollAnchor() { + const scrollContainer = document.querySelector('.chat-messages-container'); + if (!scrollContainer) return null; + + if (isChatVisuallyNearBottom(scrollContainer)) { + return { scrollContainer, stickToBottom: true }; + } + + const containerRect = scrollContainer.getBoundingClientRect(); + const visibleMessages = Array.from( + scrollContainer.querySelectorAll('.message-item[data-message-id]') + ) + .map(element => ({ element, rect: element.getBoundingClientRect() })) + .filter(({ rect }) => rect.bottom > containerRect.top && rect.top < containerRect.bottom) + .sort((a, b) => a.rect.top - b.rect.top); + + const anchor = visibleMessages[0]; + if (!anchor) { + return { + scrollContainer, + stickToBottom: false, + scrollTop: scrollContainer.scrollTop + }; + } + + return { + scrollContainer, + stickToBottom: false, + messageId: anchor.element.dataset.messageId, + viewportOffset: anchor.rect.top - containerRect.top, + scrollTop: scrollContainer.scrollTop + }; + } + + function restoreChatPresentationScrollAnchor(anchor) { + if (!anchor?.scrollContainer?.isConnected) return; + + const { scrollContainer } = anchor; + if (anchor.stickToBottom) { + const messages = scrollContainer.querySelector('.chat-messages'); + if (!messages) return; + const containerRect = scrollContainer.getBoundingClientRect(); + const messagesRect = messages.getBoundingClientRect(); + scrollContainer.scrollTop += messagesRect.bottom - containerRect.bottom; + return; + } + + const anchorElement = anchor.messageId + ? scrollContainer.querySelector(`.message-item[data-message-id="${CSS.escape(anchor.messageId)}"]`) + : null; + + if (!anchorElement) { + scrollContainer.scrollTop = anchor.scrollTop; + return; + } + + const containerRect = scrollContainer.getBoundingClientRect(); + const currentOffset = anchorElement.getBoundingClientRect().top - containerRect.top; + scrollContainer.scrollTop += currentOffset - anchor.viewportOffset; + } + + function syncChatPresentationModeControls(mode = globalSettings.chatPresentationMode) { + const normalizedMode = normalizeChatPresentationMode(mode); + const modeControl = document.querySelector( + `input[name="chatPresentationMode"][value="${normalizedMode}"]` + ); + if (modeControl) modeControl.checked = true; + + document.querySelectorAll('.chat-presentation-quick-option').forEach((option) => { + const isActive = option.dataset.presentationMode === normalizedMode; + option.classList.toggle('active', isActive); + option.setAttribute('aria-checked', String(isActive)); + option.tabIndex = isActive ? 0 : -1; + }); + + const bubbleOnlySettings = document.getElementById('userChatBubbleSettings'); + if (bubbleOnlySettings) { + bubbleOnlySettings.hidden = normalizedMode !== 'bubble'; + } + + const bubbleWidthSettings = document.getElementById('chatBubbleWidthSettings'); + if (bubbleWidthSettings) { + bubbleWidthSettings.hidden = normalizedMode !== 'bubble'; + } + } + + function setupChatPresentationQuickSwitcher() { + globalSettings = settingsOwner.get(); + document.querySelectorAll('.chat-presentation-quick-switcher').forEach((switcher) => { + const options = Array.from(switcher.querySelectorAll('.chat-presentation-quick-option')); + if (!options.length || switcher.dataset.bound === 'true') return; + const trigger = document.querySelector(`[aria-controls="${switcher.id}"]`); + const usesExplicitState = switcher.classList.contains('next-ui-chat-presentation-switcher'); + + const setOpen = (open) => { + switcher.classList.toggle('is-open', open); + trigger?.setAttribute('aria-expanded', String(open)); + }; + + const selectMode = async (option) => { + const mode = option?.dataset.presentationMode; + if (!mode) return; + await applyChatPresentationMode(mode, { + persist: true, + preserveScroll: true, + notify: false, + source: `${switcher.id || 'chat-presentation'}-quick-switcher` + }); + }; + + if (usesExplicitState && trigger) { + listenerOwner.add(trigger, 'click', (event) => { + event.stopPropagation(); + const open = !switcher.classList.contains('is-open'); + setOpen(open); + if (open) { + options.find(option => option.getAttribute('aria-checked') === 'true')?.focus(); + } + }); + listenerOwner.add(document, 'pointerdown', (event) => { + if (!switcher.classList.contains('is-open')) return; + if (switcher.contains(event.target) || trigger.contains(event.target)) return; + setOpen(false); + }); + listenerOwner.add(document, 'keydown', (event) => { + if (event.key !== 'Escape' || !switcher.classList.contains('is-open')) return; + event.preventDefault(); + event.stopPropagation(); + setOpen(false); + trigger.focus(); + }); + } + + options.forEach((option) => { + listenerOwner.add(option, 'click', async () => { + await selectMode(option); + if (usesExplicitState) setOpen(false); + }); + listenerOwner.add(option, 'keydown', (event) => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); + + const currentIndex = Math.max(0, options.indexOf(option)); + let nextIndex = currentIndex; + if (event.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + options.length) % options.length; + if (event.key === 'ArrowRight') nextIndex = (currentIndex + 1) % options.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = options.length - 1; + + options[nextIndex].focus(); + selectMode(options[nextIndex]); + }); + }); + + listenerOwner.add(switcher, 'keydown', (event) => { + if (event.key !== 'Escape') return; + event.preventDefault(); + event.stopPropagation(); + if (usesExplicitState) setOpen(false); + trigger?.focus(); + if (!usesExplicitState) trigger?.blur(); + }); + + switcher.dataset.bound = 'true'; + }); + syncChatPresentationModeControls(globalSettings.chatPresentationMode); + } + + async function applyChatPresentationMode(mode, options = {}) { return themeOwner.applyPresentation(mode, options); } + + + function clampChatBubbleWidthPercent(value, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(98, Math.max(50, parsed)); + } + + const CHAT_FONT_PRESETS = Object.freeze({ + system: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + segoe: '"Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif', + ubuntu: '"Ubuntu", "Segoe UI", "Microsoft YaHei UI", sans-serif', + yahei: '"Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI", sans-serif', + pingfang: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", sans-serif', + 'source-han': '"Source Han Sans SC", "Noto Sans CJK SC", "Microsoft YaHei UI", sans-serif', + serif: '"Noto Serif SC", "Source Han Serif SC", "Songti SC", Georgia, serif' + }); + + const CHAT_CODE_FONT_PRESETS = Object.freeze({ + cascadia: '"Cascadia Code", "Consolas", "JetBrains Mono", monospace', + fira: '"Fira Code", "Consolas", "JetBrains Mono", monospace', + consolas: '"Consolas", "Monaco", "Courier New", monospace', + system: 'ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + jetbrains: '"JetBrains Mono", "Cascadia Code", "Fira Code", "Consolas", monospace', + monaspace: '"Monaspace Neon", "JetBrains Mono", "Cascadia Code", monospace' + }); + + const TOOL_CARD_FONT_PRESETS = Object.freeze({ + ...CHAT_FONT_PRESETS, + cascadia: CHAT_CODE_FONT_PRESETS.cascadia, + fira: CHAT_CODE_FONT_PRESETS.fira, + consolas: CHAT_CODE_FONT_PRESETS.consolas, + jetbrains: CHAT_CODE_FONT_PRESETS.jetbrains, + monaspace: CHAT_CODE_FONT_PRESETS.monaspace + }); + + function sanitizeFontFamilyValue(value) { + if (typeof value !== 'string') return ''; + return value.trim().replace(/[\r\n]+/g, ' '); + } + + function resolveFontFamilyFromPreset(presetMap, presetKey, customValue, fallbackKey) { + const normalizedPreset = typeof presetKey === 'string' ? presetKey : fallbackKey; + if (normalizedPreset === 'custom') { + return sanitizeFontFamilyValue(customValue) || presetMap[fallbackKey]; + } + return presetMap[normalizedPreset] || presetMap[fallbackKey]; + } + + function resolveChatFontFamily(settings = globalSettings) { + return resolveFontFamilyFromPreset( + CHAT_FONT_PRESETS, + settings?.chatFontPreset, + settings?.chatFontCustom, + 'system' + ); + } + + function resolveChatCodeFontFamily(settings = globalSettings) { + return resolveFontFamilyFromPreset( + CHAT_CODE_FONT_PRESETS, + settings?.chatCodeFontPreset, + settings?.chatCodeFontCustom, + 'consolas' + ); + } + + function resolveDiaryFontFamily(settings = globalSettings) { + return resolveFontFamilyFromPreset( + CHAT_FONT_PRESETS, + settings?.chatDiaryFontPreset, + settings?.chatDiaryFontCustom, + 'serif' + ); + } + + function resolveToolFontFamily(settings = globalSettings) { + return resolveFontFamilyFromPreset( + TOOL_CARD_FONT_PRESETS, + settings?.chatToolFontPreset, + settings?.chatToolFontCustom, + 'system' + ); + } + + function syncChatFontControl(selectId, customRowId) { + const presetSelect = document.getElementById(selectId); + const customRow = document.getElementById(customRowId); + if (!presetSelect || !customRow) return; + customRow.style.display = presetSelect.value === 'custom' ? 'block' : 'none'; + } + + function updateFontScenarioPreview() { + const bodyFontFamily = resolveChatFontFamily({ + chatFontPreset: document.getElementById('chatFontPreset')?.value || 'system', + chatFontCustom: document.getElementById('chatFontCustom')?.value || '' + }); + const codeFontFamily = resolveChatCodeFontFamily({ + chatCodeFontPreset: document.getElementById('chatCodeFontPreset')?.value || 'consolas', + chatCodeFontCustom: document.getElementById('chatCodeFontCustom')?.value || '' + }); + const diaryFontFamily = resolveDiaryFontFamily({ + chatDiaryFontPreset: document.getElementById('chatDiaryFontPreset')?.value || 'serif', + chatDiaryFontCustom: document.getElementById('chatDiaryFontCustom')?.value || '' + }); + const toolFontFamily = resolveToolFontFamily({ + chatToolFontPreset: document.getElementById('chatToolFontPreset')?.value || 'system', + chatToolFontCustom: document.getElementById('chatToolFontCustom')?.value || '' + }); + + const bodyEl = document.getElementById('scenarioPreviewBody'); + const codeEl = document.getElementById('scenarioPreviewCode'); + const diaryEl = document.getElementById('scenarioPreviewDiary'); + const toolEl = document.getElementById('scenarioPreviewTool'); + + if (bodyEl) bodyEl.style.fontFamily = bodyFontFamily; + if (codeEl) codeEl.style.fontFamily = codeFontFamily; + if (diaryEl) diaryEl.style.fontFamily = diaryFontFamily; + if (toolEl) toolEl.style.fontFamily = toolFontFamily; + } + + function ensureScenarioFontSettingsMount(previewId, mountId) { + const previewEl = document.getElementById(previewId); + if (!previewEl) return null; + + const card = previewEl.closest('.scenario-preview-card'); + if (!card) return null; + + let mountEl = document.getElementById(mountId); + if (!mountEl) { + mountEl = document.createElement('div'); + mountEl.id = mountId; + mountEl.className = 'scenario-preview-settings-slot'; + const noteEl = card.querySelector('.scenario-preview-note'); + if (noteEl) { + card.insertBefore(mountEl, noteEl); + } else { + card.appendChild(mountEl); + } + } + + return mountEl; + } + + function mountChatFontSettingGroups() { + [ + { + groupId: 'chatFontSettingsGroup', + previewId: 'scenarioPreviewBody', + mountId: 'chatFontSettingsMount' + }, + { + groupId: 'chatCodeFontSettingsGroup', + previewId: 'scenarioPreviewCode', + mountId: 'chatCodeFontSettingsMount' + } + ].forEach(({ groupId, previewId, mountId }) => { + const groupEl = document.getElementById(groupId); + const mountEl = ensureScenarioFontSettingsMount(previewId, mountId); + if (!groupEl || !mountEl) return; + + if (groupEl.parentElement !== mountEl) { + mountEl.appendChild(groupEl); + } + + groupEl.style.marginTop = '0'; + groupEl.style.marginBottom = '0'; + }); + } + + function syncChatFontControls() { + mountChatFontSettingGroups(); + syncChatFontControl('chatFontPreset', 'chatFontCustomRow'); + syncChatFontControl('chatCodeFontPreset', 'chatCodeFontCustomRow'); + syncChatFontControl('chatDiaryFontPreset', 'chatDiaryFontCustomRow'); + syncChatFontControl('chatToolFontPreset', 'chatToolFontCustomRow'); + updateFontScenarioPreview(); + } + + function syncWideChatLayoutControls() { + const wideModeRadio = document.getElementById('chatLayoutModeWide'); + const widthSettings = document.getElementById('chatBubbleWidthSettings'); + if (!wideModeRadio || !widthSettings) return; + widthSettings.style.display = wideModeRadio.checked ? 'block' : 'none'; + } + + function syncUserChatBubbleControls() { + const bubbleUiToggle = document.getElementById('enableUserChatBubbleUi'); + const metaSettings = document.getElementById('userChatBubbleMetaSettings'); + if (!bubbleUiToggle || !metaSettings) return; + metaSettings.style.display = bubbleUiToggle.checked ? 'flex' : 'none'; + } + + function applyUserChatBubbleUiState(settings = globalSettings) { + document.querySelectorAll('.message-item.user').forEach((messageItem) => { + applyUserMessageLayoutState(messageItem, settings); + }); + } + + function applyChatBubbleLayoutSettings(settings = globalSettings) { + const rootStyle = document.documentElement.style; + const resolvedSettings = settings || {}; + const chatFontFamily = resolveChatFontFamily(resolvedSettings); + const chatCodeFontFamily = resolveChatCodeFontFamily(resolvedSettings); + const diaryFontFamily = resolveDiaryFontFamily(resolvedSettings); + const toolFontFamily = resolveToolFontFamily(resolvedSettings); + + const defaultWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthDefault, 82); + const notificationsWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthNotifications, 90); + const narrowWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthNarrow, 85); + const wideDefaultWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthWideDefault, 92); + const wideNotificationsWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthWideNotifications, 96); + const wideNarrowWidth = clampChatBubbleWidthPercent( + resolvedSettings.chatBubbleMaxWidthWideNarrow, + wideDefaultWidth + ); + + rootStyle.setProperty('--chat-bubble-max-width', `${defaultWidth}%`); + rootStyle.setProperty('--chat-bubble-max-width-notifications', `${notificationsWidth}%`); + rootStyle.setProperty('--chat-bubble-max-width-narrow', `${narrowWidth}%`); + rootStyle.setProperty('--chat-bubble-max-width-wide', `${wideDefaultWidth}%`); + rootStyle.setProperty('--chat-bubble-max-width-wide-notifications', `${wideNotificationsWidth}%`); + rootStyle.setProperty('--chat-bubble-max-width-wide-narrow', `${wideNarrowWidth}%`); + rootStyle.setProperty('--vcp-chat-font-family', chatFontFamily); + rootStyle.setProperty('--vcp-chat-code-font-family', chatCodeFontFamily); + rootStyle.setProperty('--vcp-diary-font-family', diaryFontFamily); + rootStyle.setProperty('--vcp-tool-card-font-family', toolFontFamily); + rootStyle.setProperty('--font-family', chatFontFamily); + rootStyle.setProperty('--font-family-sans-serif', chatFontFamily); + rootStyle.setProperty('--font-family-monospace', chatCodeFontFamily); + + if (getPretextBridge() && typeof getPretextBridge().setChatFonts === 'function') { + getPretextBridge().setChatFonts(chatFontFamily, chatCodeFontFamily); + } + + if (document.body) { + document.body.classList.toggle('chat-wide-layout', resolvedSettings.enableWideChatLayout === true); + } + applyChatPresentationMode(resolvedSettings.chatPresentationMode, { + persist: false, + preserveScroll: false, + source: 'layout-settings' + }); + applyUserChatBubbleUiState(resolvedSettings); + } + + /** + * 🟢 将全局设置同步到 UI 元素(仅在模态框实例化后调用) + */ + async function syncGlobalSettingsToUI() { + const token = generation; + if (!isCurrent(token)) return false; + globalSettings = settingsOwner.get(); + const safeSet = (id, value, prop = 'value') => { + const el = document.getElementById(id); + if (el) el[prop] = value; + }; + const safeCheck = (id, checked) => { + const el = document.getElementById(id); + if (el) el.checked = !!checked; + }; + const syncRustDebugPanelVisibility = () => { + const rustDebugModeEl = document.getElementById('rustDebugMode'); + const rustDebugPanelEl = document.getElementById('rustDebugPanel'); + if (rustDebugPanelEl) { + rustDebugPanelEl.style.display = rustDebugModeEl?.checked ? 'block' : 'none'; + } + }; + const joinKeywords = (value) => Array.isArray(value) ? value.join('\n') : ''; + const shouldShowRustGuardRules = () => { + const useRust = document.getElementById('rustUseAssistant')?.checked === true; + return useRust; + }; + const syncRustGuardRulesVisibility = () => { + const container = document.getElementById('rustGuardRulesContainer'); + if (container) { + container.style.display = shouldShowRustGuardRules() ? 'block' : 'none'; + } + }; + + safeSet('userName', globalSettings.userName || '用户'); + + const borderColor = globalSettings.userAvatarBorderColor || '#3d5a80'; + safeSet('userAvatarBorderColor', borderColor); + safeSet('userAvatarBorderColorText', borderColor); + + const nameColor = globalSettings.userNameTextColor || '#ffffff'; + safeSet('userNameTextColor', nameColor); + safeSet('userNameTextColorText', nameColor); + + safeCheck('userUseThemeColorsInChat', globalSettings.userUseThemeColorsInChat); + + const completedUrl = getSettingsManager().completeVcpUrl(globalSettings.vcpServerUrl || ''); + safeSet('vcpServerUrl', completedUrl); + safeSet('vcpApiKey', globalSettings.vcpApiKey || ''); + safeSet('fileKey', globalSettings.fileKey || ''); + safeSet('vcpLogUrl', globalSettings.vcpLogUrl || ''); + safeSet('vcpLogKey', globalSettings.vcpLogKey || ''); + safeSet('topicSummaryModel', globalSettings.topicSummaryModel || ''); + safeSet('continueWritingPrompt', globalSettings.continueWritingPrompt || '请继续'); + safeSet('flowlockContinueDelay', globalSettings.flowlockContinueDelay ?? 5); + safeCheck('voiceModeLocal', (globalSettings.voiceMode || 'local') !== 'network'); + safeCheck('voiceModeNetwork', (globalSettings.voiceMode || 'local') === 'network'); + safeSet('speechRecognizerBrowserPath', globalSettings.speechRecognizerBrowserPath || ''); + safeSet('speechRecognizerPagePath', globalSettings.speechRecognizerPagePath || 'Voicechatmodules/recognizer.html'); + safeSet('voiceLocalSovitsUrl', globalSettings.voiceLocalSettings?.sovitsUrl || ''); + safeSet('voiceLocalSovitsKey', globalSettings.voiceLocalSettings?.sovitsKey || ''); + safeSet('voiceNetworkProviderUrl', globalSettings.voiceNetworkSettings?.providerUrl || ''); + safeSet('voiceNetworkProviderKey', globalSettings.voiceNetworkSettings?.providerKey || ''); + + // Network Notes Paths + const networkNotesPathsContainer = document.getElementById('networkNotesPathsContainer'); + if (networkNotesPathsContainer) { + networkNotesPathsContainer.innerHTML = ''; + const paths = Array.isArray(globalSettings.networkNotesPaths) ? globalSettings.networkNotesPaths : (globalSettings.networkNotesPath ? [globalSettings.networkNotesPath] : []); + if (paths.length === 0) { + uiHelperFunctions.addNetworkPathInput(''); + } else { + paths.forEach(path => uiHelperFunctions.addNetworkPathInput(path)); + } + } + + safeCheck('enableSmoothStreaming', globalSettings.enableSmoothStreaming === true); + safeCheck('showHomeVisualBrand', globalSettings.showHomeVisualBrand !== false); + safeCheck('showHomeVisualTagline', globalSettings.showHomeVisualTagline !== false); + safeSet('homeVisualTagline', globalSettings.homeVisualTagline || '语义级打穿 AI、UI/UX、APP 与人类想象力的边界'); + const appearance = getAppearance()?.normalize(globalSettings.appearanceProfile, 'next'); + safeSet('appearanceDensity', appearance?.density || 'comfortable'); + safeSet('appearanceRadius', appearance?.radius || 'small'); + safeSet('appearanceTypography', appearance?.typography || 'system'); + safeSet('appearanceFontScale', appearance?.fontScale || 'normal'); + safeSet('appearanceContentWidth', appearance?.contentWidth || 'full'); + safeSet('appearanceSidebarRowHeight', appearance?.sidebarRowHeight ?? 46); + safeSet('appearanceSidebarRowHeightValue', `${appearance?.sidebarRowHeight ?? 46}px`); + safeSet('appearanceSidebarAvatarSize', appearance?.sidebarAvatarSize ?? 32); + safeSet('appearanceSidebarAvatarSizeValue', `${appearance?.sidebarAvatarSize ?? 32}px`); + safeSet('appearanceSidebarRadius', appearance?.sidebarRadius || 'tuned'); + safeCheck(`appearanceSidebarRadiusChoice-${appearance?.sidebarRadius || 'tuned'}`, true); + safeSet('appearanceCustomRadius', appearance?.customRadius ?? 10); + safeSet('appearanceCustomRadiusValue', `${appearance?.customRadius ?? 10}px`); + document.getElementById('appearanceSidebarAvatarSize')?.dispatchEvent(new Event('input', { bubbles: true })); + safeSet('appearanceSurface', appearance?.surface || 'translucent'); + safeSet('chatFontPreset', globalSettings.chatFontPreset || 'system'); + safeSet('chatFontCustom', globalSettings.chatFontCustom || ''); + safeSet('chatCodeFontPreset', globalSettings.chatCodeFontPreset || 'consolas'); + safeSet('chatCodeFontCustom', globalSettings.chatCodeFontCustom || ''); + safeSet('chatDiaryFontPreset', globalSettings.chatDiaryFontPreset || 'serif'); + safeSet('chatDiaryFontCustom', globalSettings.chatDiaryFontCustom || ''); + safeSet('chatToolFontPreset', globalSettings.chatToolFontPreset || 'system'); + safeSet('chatToolFontCustom', globalSettings.chatToolFontCustom || ''); + const presentationMode = normalizeChatPresentationMode(globalSettings.chatPresentationMode); + safeCheck(`chatPresentationMode${presentationMode[0].toUpperCase()}${presentationMode.slice(1)}`, true); + safeCheck('chatLayoutModeWide', globalSettings.enableWideChatLayout === true); + safeCheck('chatLayoutModeNormal', globalSettings.enableWideChatLayout !== true); + safeCheck('enableUserChatBubbleUi', globalSettings.enableUserChatBubbleUi !== false); + safeCheck('showUserMetaInChatBubbleUi', globalSettings.showUserMetaInChatBubbleUi !== false); + safeSet('chatBubbleMaxWidthWideDefault', clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideDefault, 92)); + safeSet('chatBubbleMaxWidthWideNotifications', clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideNotifications, 96)); + safeSet( + 'chatBubbleMaxWidthWideNarrow', + clampChatBubbleWidthPercent( + globalSettings.chatBubbleMaxWidthWideNarrow, + clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideDefault, 92) + ) + ); + safeSet('minChunkBufferSize', globalSettings.minChunkBufferSize ?? 16); + safeSet('smoothStreamIntervalMs', globalSettings.smoothStreamIntervalMs ?? 100); + syncChatFontControls(); + syncWideChatLayoutControls(); + syncUserChatBubbleControls(); + syncChatPresentationModeControls(presentationMode); + + const chatFontPresetSelect = document.getElementById('chatFontPreset'); + const chatFontCustomInput = document.getElementById('chatFontCustom'); + const chatCodeFontPresetSelect = document.getElementById('chatCodeFontPreset'); + const chatCodeFontCustomInput = document.getElementById('chatCodeFontCustom'); + const chatDiaryFontPresetSelect = document.getElementById('chatDiaryFontPreset'); + const chatDiaryFontCustomInput = document.getElementById('chatDiaryFontCustom'); + const chatToolFontPresetSelect = document.getElementById('chatToolFontPreset'); + const chatToolFontCustomInput = document.getElementById('chatToolFontCustom'); + const wideModeRadio = document.getElementById('chatLayoutModeWide'); + const normalModeRadio = document.getElementById('chatLayoutModeNormal'); + const userBubbleUiToggle = document.getElementById('enableUserChatBubbleUi'); + const presentationModeRadios = document.querySelectorAll('input[name="chatPresentationMode"]'); + if (chatFontPresetSelect && !chatFontPresetSelect.dataset.boundFontToggle) { + listenerOwner.add(chatFontPresetSelect, 'change', syncChatFontControls); + chatFontPresetSelect.dataset.boundFontToggle = 'true'; + } + if (chatCodeFontPresetSelect && !chatCodeFontPresetSelect.dataset.boundFontToggle) { + listenerOwner.add(chatCodeFontPresetSelect, 'change', syncChatFontControls); + chatCodeFontPresetSelect.dataset.boundFontToggle = 'true'; + } + if (chatDiaryFontPresetSelect && !chatDiaryFontPresetSelect.dataset.boundFontToggle) { + listenerOwner.add(chatDiaryFontPresetSelect, 'change', syncChatFontControls); + chatDiaryFontPresetSelect.dataset.boundFontToggle = 'true'; + } + if (chatToolFontPresetSelect && !chatToolFontPresetSelect.dataset.boundFontToggle) { + listenerOwner.add(chatToolFontPresetSelect, 'change', syncChatFontControls); + chatToolFontPresetSelect.dataset.boundFontToggle = 'true'; + } + if (chatFontCustomInput && !chatFontCustomInput.dataset.boundFontPreview) { + listenerOwner.add(chatFontCustomInput, 'input', updateFontScenarioPreview); + chatFontCustomInput.dataset.boundFontPreview = 'true'; + } + if (chatCodeFontCustomInput && !chatCodeFontCustomInput.dataset.boundFontPreview) { + listenerOwner.add(chatCodeFontCustomInput, 'input', updateFontScenarioPreview); + chatCodeFontCustomInput.dataset.boundFontPreview = 'true'; + } + if (chatDiaryFontCustomInput && !chatDiaryFontCustomInput.dataset.boundFontPreview) { + listenerOwner.add(chatDiaryFontCustomInput, 'input', updateFontScenarioPreview); + chatDiaryFontCustomInput.dataset.boundFontPreview = 'true'; + } + if (chatToolFontCustomInput && !chatToolFontCustomInput.dataset.boundFontPreview) { + listenerOwner.add(chatToolFontCustomInput, 'input', updateFontScenarioPreview); + chatToolFontCustomInput.dataset.boundFontPreview = 'true'; + } + if (wideModeRadio && !wideModeRadio.dataset.boundWideChatToggle) { + listenerOwner.add(wideModeRadio, 'change', syncWideChatLayoutControls); + wideModeRadio.dataset.boundWideChatToggle = 'true'; + } + if (normalModeRadio && !normalModeRadio.dataset.boundWideChatToggle) { + listenerOwner.add(normalModeRadio, 'change', syncWideChatLayoutControls); + normalModeRadio.dataset.boundWideChatToggle = 'true'; + } + if (userBubbleUiToggle && !userBubbleUiToggle.dataset.boundUserBubbleToggle) { + listenerOwner.add(userBubbleUiToggle, 'change', syncUserChatBubbleControls); + userBubbleUiToggle.dataset.boundUserBubbleToggle = 'true'; + } + presentationModeRadios.forEach((radio) => { + if (radio.dataset.boundPresentationModeToggle) return; + listenerOwner.add(radio, 'change', () => { + if (!radio.checked) return; + applyChatPresentationMode(radio.value, { + persist: true, + preserveScroll: true, + notify: false, + source: 'settings' + }); + }); + radio.dataset.boundPresentationModeToggle = 'true'; + }); + + // User Avatar Preview + const userAvatarPreview = document.getElementById('userAvatarPreview'); + const userAvatarWrapper = userAvatarPreview?.closest('.agent-avatar-wrapper'); + if (userAvatarPreview) { + if (globalSettings.userAvatarUrl) { + userAvatarPreview.src = globalSettings.userAvatarUrl; + userAvatarPreview.style.display = 'block'; + userAvatarWrapper?.classList.remove('no-avatar'); + } else { + userAvatarPreview.src = '#'; + userAvatarPreview.style.display = 'none'; + userAvatarWrapper?.classList.add('no-avatar'); + } + } + + // 加载论坛配置并填充管理员账号/密码 + try { + const forumConfig = await chatAPI.loadForumConfig(); + if (!isCurrent(token)) return false; + if (forumConfig && !forumConfig.error) { + safeSet('adminUsername', forumConfig.username || ''); + safeSet('adminPassword', forumConfig.password || ''); + } + } catch (err) { + console.warn('[Renderer] Failed to load forum config for global settings:', err); + } + + // Assistant Select + const assistantAgentSelect = document.getElementById('assistantAgent'); + if (assistantAgentSelect) { + await getSettingsManager().populateAssistantAgentSelect(); + if (!isCurrent(token)) return false; + assistantAgentSelect.value = globalSettings.assistantAgent || ''; + } + + safeCheck('enableDistributedServer', globalSettings.enableDistributedServer === true); + safeCheck('agentMusicControl', globalSettings.agentMusicControl === true); + safeCheck('enableVcpToolInjection', globalSettings.enableVcpToolInjection === true); + safeCheck('enableThoughtChainInjection', globalSettings.enableThoughtChainInjection === true); + safeCheck('enableContextSanitizer', globalSettings.enableContextSanitizer === true); + safeSet('contextSanitizerDepth', globalSettings.contextSanitizerDepth ?? 2); + + const contextSanitizerDepthContainer = document.getElementById('contextSanitizerDepthContainer'); + if (contextSanitizerDepthContainer) { + contextSanitizerDepthContainer.style.display = globalSettings.enableContextSanitizer === true ? 'block' : 'none'; + } + + safeCheck('enableAiMessageButtons', globalSettings.enableAiMessageButtons !== false); + safeCheck('enableMiddleClickQuickAction', globalSettings.enableMiddleClickQuickAction === true); + safeSet('middleClickQuickAction', globalSettings.middleClickQuickAction || ''); + safeCheck('enableMiddleClickAdvanced', globalSettings.enableMiddleClickAdvanced === true); + safeSet('middleClickAdvancedDelay', Math.max(1000, globalSettings.middleClickAdvancedDelay ?? 1000)); + safeCheck('enableRegenerateConfirmation', globalSettings.enableRegenerateConfirmation !== false); + + if (chatAPI?.getRustAssistantConfig) { + try { + const rustConfig = await chatAPI.getRustAssistantConfig(); + if (!isCurrent(token)) return false; + if (rustConfig && !rustConfig.error) { + safeCheck('rustUseAssistant', rustConfig.useRustAssistant === true); + safeCheck('rustDebugMode', rustConfig.debugMode === true); + safeSet('rustWhitelistKeywords', joinKeywords(rustConfig.whitelist || [])); + safeSet('rustBlacklistKeywords', joinKeywords(rustConfig.blacklist || [])); + safeSet('rustScreenshotApps', joinKeywords(rustConfig.screenshotApps || [])); + syncRustDebugPanelVisibility(); + syncRustGuardRulesVisibility(); + + const rustDebugModeEl = document.getElementById('rustDebugMode'); + if (rustDebugModeEl && !rustDebugModeEl.dataset.debugPanelBound) { + listenerOwner.add(rustDebugModeEl, 'change', syncRustDebugPanelVisibility); + rustDebugModeEl.dataset.debugPanelBound = 'true'; + } + + const rustUseAssistantEl = document.getElementById('rustUseAssistant'); + if (rustUseAssistantEl && !rustUseAssistantEl.dataset.guardPanelBound) { + listenerOwner.add(rustUseAssistantEl, 'change', syncRustGuardRulesVisibility); + rustUseAssistantEl.dataset.guardPanelBound = 'true'; + } + + } + } catch (error) { + console.warn('[Renderer] Failed to sync Rust assistant config:', error); + } + } + + if (chatAPI?.getAssistantRuntimeStatus && document.getElementById('rustDebugMode')?.checked) { + try { + const runtime = await chatAPI.getAssistantRuntimeStatus(); + if (!isCurrent(token)) return false; + if (runtime && runtime.success) { + const modeText = runtime.mode === 'rust' + ? 'Rust' + : (runtime.mode === 'disabled' ? 'Disabled' : runtime.mode || 'Unknown'); + const desiredText = runtime.desiredMode === 'rust' + ? 'Rust' + : (runtime.desiredMode === 'disabled' ? 'Disabled' : runtime.desiredMode || 'Unknown'); + const activeText = runtime.active ? '运行中' : '未运行'; + const debugReasonText = runtime.lastDebugReason || '无'; + const forwardedCount = runtime.forwardedEventCount || 0; + const sidecarActiveText = runtime.rustSidecarListenerActive === null + ? '未知' + : (runtime.rustSidecarListenerActive ? '是' : '否'); + const processAliveText = runtime.adapterProcessAlive ? '运行中' : '未运行'; + const processPidText = runtime.adapterProcessPid ? String(runtime.adapterProcessPid) : '无'; + const autoFallbackCount = runtime.runtimeFallbackTrace?.autoFallbackCount || 0; + const autoFallbackReason = runtime.runtimeFallbackTrace?.lastAutoFallbackReason || '无'; + const receivedCount = runtime.integrationTrace?.receivedSelectionCount || 0; + const showAttemptCount = runtime.integrationTrace?.showAttemptCount || 0; + const showErrorText = runtime.integrationTrace?.lastShowError || '无'; + safeSet('assistantRuntimeMode', modeText, 'textContent'); + safeSet('assistantRuntimeDesiredMode', desiredText, 'textContent'); + safeSet('assistantRuntimeActive', activeText, 'textContent'); + safeSet('assistantRuntimeDebugReason', debugReasonText, 'textContent'); + safeSet('assistantRuntimeForwardedCount', String(forwardedCount), 'textContent'); + safeSet('assistantRuntimeSidecarActive', sidecarActiveText, 'textContent'); + safeSet('assistantRuntimeProcessAlive', processAliveText, 'textContent'); + safeSet('assistantRuntimeProcessPid', processPidText, 'textContent'); + safeSet('assistantRuntimeAutoFallbackCount', String(autoFallbackCount), 'textContent'); + safeSet('assistantRuntimeAutoFallbackReason', autoFallbackReason, 'textContent'); + safeSet('assistantRuntimeReceivedCount', String(receivedCount), 'textContent'); + safeSet('assistantRuntimeShowAttemptCount', String(showAttemptCount), 'textContent'); + safeSet('assistantRuntimeShowError', showErrorText, 'textContent'); + } + } catch (error) { + console.warn('[Renderer] Failed to load assistant runtime status:', error); + } + } + + // Visibility toggles + const middleClickContainer = document.getElementById('middleClickQuickActionContainer'); + if (middleClickContainer) middleClickContainer.style.display = globalSettings.enableMiddleClickQuickAction ? 'block' : 'none'; + const middleClickAdvancedContainer = document.getElementById('middleClickAdvancedContainer'); + if (middleClickAdvancedContainer) middleClickAdvancedContainer.style.display = globalSettings.enableMiddleClickQuickAction ? 'block' : 'none'; + const middleClickAdvancedSettings = document.getElementById('middleClickAdvancedSettings'); + if (middleClickAdvancedSettings) middleClickAdvancedSettings.style.display = globalSettings.enableMiddleClickAdvanced ? 'block' : 'none'; + return true; + } + + return Object.freeze({ + configureThemeOwner(owner) { + if (!owner || typeof owner.applyPresentation !== 'function') { + throw new TypeError('MainChatSettingsPresentationOwner requires a theme owner.'); + } + if (themeOwner && themeOwner !== owner) throw new Error('Theme owner is already configured.'); + themeOwner = owner; + }, + configureStartup({ loadSettings: nextLoadSettings, startupThemeGate: nextStartupThemeGate } = {}) { + if (typeof nextLoadSettings !== 'function' || !nextStartupThemeGate?.release) { + throw new TypeError('MainChatSettingsPresentationOwner requires startup settings capabilities.'); + } + loadSettingsCapability = nextLoadSettings; + startupThemeGateCapability = nextStartupThemeGate; + }, + applyInitialTheme: mode => themeOwner.applyInitialTheme(mode), + normalizePresentation: mode => themeOwner.normalizePresentation(mode), + applyPresentation: (mode, options) => themeOwner.applyPresentation(mode, options), + capturePresentationAnchor: captureChatPresentationScrollAnchor, + restorePresentationAnchor: restoreChatPresentationScrollAnchor, + syncPresentationControls: syncChatPresentationModeControls, + setupPresentationQuickSwitcher: setupChatPresentationQuickSwitcher, + applyLayoutSettings: applyChatBubbleLayoutSettings, + syncSettingsToUI: () => track(syncGlobalSettingsToUI()), + loadAndApply: () => track(loadAndApply()), + async dispose() { + if (disposed) return; + disposed = true; + generation += 1; + await Promise.allSettled([...tasks]); + themeOwner = null; + loadSettingsCapability = null; + startupThemeGateCapability = null; + }, + }); +} diff --git a/modules/renderer/mainChatStreamConsumer.js b/modules/renderer/mainChatStreamConsumer.js new file mode 100644 index 000000000..f738086cd --- /dev/null +++ b/modules/renderer/mainChatStreamConsumer.js @@ -0,0 +1,91 @@ +import { normalizeStreamChunk } from '../chat/contentRuntime.js'; + +const terminalTypes = new Set(['completed', 'failed', 'cancelled', 'discarded']); + +const buildMessage = event => { + const context = event.context || {}; + return { + id: event.messageId, + role: 'assistant', + name: context.agentName, + agentId: context.agentId, + avatarUrl: context.avatarUrl, + avatarColor: context.avatarColor, + content: event.type === 'agent_thinking' ? '思考中...' : '', + timestamp: Date.now(), + isThinking: event.type === 'agent_thinking', + isGroupMessage: context.isGroupMessage || false, + groupId: context.groupId, + topicId: context.topicId, + context, + streamOperationId: event.streamOperationId || event.operationId || null, + }; +}; + +/** Main-window projection of stream facts. All powers are explicit capabilities. */ +export function createMainChatStreamConsumer(initialEvent, capabilities) { + const { messageId } = initialEvent; + const operationId = initialEvent.streamOperationId || initialEvent.operationId || messageId; + let context = initialEvent.context || {}; + let preparedType = null; + const projection = capabilities.resolveProjection?.(messageId) || null; + + return Object.freeze({ + surfaceGeneration: capabilities.getSurfaceGeneration?.(), + normalizeChunk: normalizeStreamChunk, + async prepare(event) { + context = { ...context, ...(event.context || {}) }; + if (preparedType === null) { + preparedType = event.type; + await (projection?.start || capabilities.start)(buildMessage({ ...event, context, streamOperationId: operationId })); + } + }, + consume(event) { + const terminal = terminalTypes.has(event.type); + if (projection?.suppressed) { + if (terminal) projection.settle?.({ event, finalized: event.outcome?.persistence?.value, context, messageId }); + return; + } + if (event.type === 'chunk') (projection?.append || capabilities.append)(messageId, event.text, context, operationId); + if (!terminal) return; + const finalized = event.outcome?.persistence?.value; + capabilities.dispatchTerminal?.({ + type: event.outcome?.transport?.kind === 'failed' ? 'error' : event.type, + messageId, + context, + error: event.outcome?.transport?.error || event.outcome?.persistence?.error || null, + }); + if (event.type === 'failed') capabilities.renderError?.({ event, finalized, context }); + projection?.settle?.({ event, finalized, context, messageId, streamOperationId: operationId }); + capabilities.onSettled?.({ event, finalized, context, messageId }); + }, + async persist(value) { + if (projection?.suppressed) return null; + const terminal = value.terminal || {}; + if (terminal.phase === 'projection') { + throw terminal.error || new Error(`Stream projection failed: ${messageId}`); + } + let fullResponse = terminal.fullResponse || value.snapshot?.text || ''; + const error = terminal.error?.message || terminal.error || ''; + if (terminal.kind === 'failed' && fullResponse.trim()) { + fullResponse += `\n\n> [!WARNING]\n> **流式响应中断**: ${error || '未知连接错误'}。已保存已接收的部分内容。`; + } + const projectTerminal = projection?.projectTerminal || capabilities.projectTerminal; + const projected = await projectTerminal( + messageId, + terminal.kind === 'completed' ? (terminal.finishReason || 'completed') : terminal.kind, + terminal.context || context, + { fullResponse, error, streamOperationId: operationId }, + ); + if (!projected) throw new Error(`Stream terminal projection failed: ${messageId}`); + const finalized = await capabilities.persistTerminal(projected); + try { + await capabilities.afterPersist?.({ terminal, finalized, context, messageId }); + } catch (sideEffectError) { + capabilities.reportError?.('[MainChatStreamConsumer] post-commit side effect failed', sideEffectError); + } + return finalized; + }, + dispose() { projection?.release?.(); }, + }); +} diff --git a/modules/renderer/mainChatSurfaceAdapter.js b/modules/renderer/mainChatSurfaceAdapter.js new file mode 100644 index 000000000..2032cfa10 --- /dev/null +++ b/modules/renderer/mainChatSurfaceAdapter.js @@ -0,0 +1,143 @@ +import { createChatSurface } from '../chat/chatSurface.js'; +import { createStreamConsumerRegistry } from '../chat/streamConsumerRegistry.js'; +import { createVcpStreamBridge } from '../chat/vcpStreamBridge.js'; +import { createMainChatStreamConsumer } from './mainChatStreamConsumer.js'; + +function createStreamCapabilities(root, services) { + const required = ['streamProjection', 'historyPersistence', 'messageRenderer', 'getSelection', 'getTopicId']; + for (const name of required) { + if (!services?.[name]) throw new TypeError(`MainChatSurfaceAdapter requires stream service: ${name}`); + } + const ownerDocument = root.ownerDocument; + const reportError = services.reportError || console.error; + return Object.freeze({ + start: message => services.streamProjection.startStreamingMessage(message), + append: (messageId, chunk, context, operationId) => services.streamProjection.appendStreamChunk(messageId, chunk, context, operationId), + projectTerminal: (messageId, finishReason, context, payload) => ( + services.streamProjection.projectStreamTerminal(messageId, finishReason, context, payload) + ), + persistTerminal: projected => services.historyPersistence.commit(projected), + dispatchTerminal: detail => services.dispatchTerminal?.(detail), + onSettled(value) { + services.notifySendStateChanged?.(value); + }, + reportError, + async afterPersist({ terminal, finalized, context, messageId }) { + const finalizedContext = finalized?.context || context; + const finalizedContent = finalized?.content || terminal.fullResponse || ''; + const selected = services.getSelection(); + const relevant = finalizedContext + && selected + && (finalizedContext.groupId ? finalizedContext.groupId === selected.id : finalizedContext.agentId === selected.id) + && finalizedContext.topicId === services.getTopicId(); + const effects = []; + if (terminal.kind === 'completed' && finalizedContext && !finalizedContext.isGroupMessage && relevant) { + effects.push(Promise.resolve().then(() => services.chatManager?.attemptTopicSummarizationIfNeeded?.())); + } + effects.push(Promise.resolve().then(() => services.flowlockManager?.handleFinalizedMessage?.({ + type: terminal.kind === 'failed' ? 'error' : 'end', messageId, + context: finalizedContext, content: finalizedContent, + finishReason: finalized?.finishReason || terminal.finishReason || terminal.kind, + error: terminal.error || null, + }))); + const results = await Promise.allSettled(effects); + results.forEach(result => { + if (result.status === 'rejected') reportError('[MainChatSurfaceAdapter] post-commit side effect failed', result.reason); + }); + }, + renderError({ event, context }) { + const selected = services.getSelection(); + const relevant = context && selected + && (context.groupId ? context.groupId === selected.id : context.agentId === selected.id) + && context.topicId === services.getTopicId(); + if (!relevant) return; + const error = event.outcome?.transport?.error?.message + || event.outcome?.transport?.error + || event.outcome?.persistence?.error?.message + || '未知连接错误'; + const errorContent = root.querySelector(`.message-item[data-message-id="${event.messageId}"] .md-content`); + if (errorContent) { + const paragraph = ownerDocument.createElement('p'); + const strong = ownerDocument.createElement('strong'); + strong.style.color = 'red'; + strong.textContent = `流错误: ${error}`; + paragraph.appendChild(strong); + errorContent.appendChild(paragraph); + } else { + services.messageRenderer.renderMessage({ role: 'system', content: `流处理错误 (ID: ${event.messageId}): ${error}`, timestamp: Date.now(), id: `err_${event.messageId}` }); + } + }, + }); +} + +/** Main-window composition owner. Domain protocols remain outside this adapter. */ +export function createMainChatSurfaceAdapter({ + root, + renderer, + repository, + focusTarget, + operations, + presentationState, + renderDependencies, + streamServices, + disposeRenderer, + ownerWindow = root?.ownerDocument?.defaultView, + onDispose = null, +}) { + if (!root || !renderer || !repository) throw new TypeError('MainChatSurfaceAdapter requires root, renderer and repository'); + const streamRoutes = createStreamConsumerRegistry(); + const surface = createChatSurface({ + root, + renderer, + repository, + focusTarget, + mode: 'interactive', + operations, + presentationState, + disposeRenderer, + }); + renderer.initializeMessageRenderer({ ...renderDependencies, chatDomRenderer: surface.renderer }); + const streamCapabilities = createStreamCapabilities(root, streamServices); + const bridge = createVcpStreamBridge({ + createConsumer: initialEvent => createMainChatStreamConsumer(initialEvent, { + ...streamCapabilities, + resolveProjection: messageId => streamRoutes.claim(messageId), + }), + }); + let disposed = false; + const unloadHandler = () => { void api.dispose(); }; + ownerWindow?.addEventListener?.('beforeunload', unloadHandler, { once: true }); + const registerStreamRoute = (messageId, route) => { + const release = streamRoutes.register(messageId, route); + const ownedRelease = () => release(); + ownedRelease.retract = () => { + streamRoutes.claim(messageId)?.settle?.({ + event: Object.freeze({ type: 'discarded', messageId, reason: 'surface-route-retracted' }), + finalized: null, + context: null, + messageId, + }); + release.retract(); + return bridge.disposeOperation(messageId, 'surface-route-retracted'); + }; + ownedRelease.cancel = reason => bridge.cancelOperation(messageId, reason || 'surface-operation-cancelled'); + return ownedRelease; + }; + const api = Object.freeze({ + surface, + domRenderer: surface.renderer, + streamRoutes: Object.freeze({ register: registerStreamRoute }), + acceptStreamEvent(event) { return disposed ? false : bridge.accept(event); }, + cancelStream(messageId, reason) { return disposed ? Promise.resolve(null) : bridge.cancelOperation(messageId, reason); }, + async dispose() { + if (disposed) return; + disposed = true; + ownerWindow?.removeEventListener?.('beforeunload', unloadHandler); + streamRoutes.dispose(); + await bridge.dispose(); + await surface.dispose(); + await onDispose?.(); + }, + }); + return api; +} diff --git a/modules/renderer/mainChatThemeOwner.js b/modules/renderer/mainChatThemeOwner.js new file mode 100644 index 000000000..d2e2bc910 --- /dev/null +++ b/modules/renderer/mainChatThemeOwner.js @@ -0,0 +1,35 @@ +const MODES = Object.freeze(['bubble', 'panel', 'immersive']); + +/** Owns main-chat theme and presentation transitions, persistence and rollback. */ +export function createMainChatThemeOwner({ settingsOwner, documentRef, presentationState, getUiManager, matchMedia, saveSettings, pretextBridge, refreshLayout, syncControls, captureAnchor, restoreAnchor, scheduleFrame, notify } = {}) { + let disposed = false; + const normalizePresentation = mode => MODES.includes(mode) ? mode : 'bubble'; + const applyInitialTheme = mode => { + if (disposed) return; + let theme = mode === 'light' || mode === 'dark' ? mode : null; + if (!theme && typeof matchMedia === 'function') theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + theme ||= 'light'; + const uiManager = getUiManager?.(); + if (uiManager?.applyTheme) uiManager.applyTheme(theme); + else { documentRef.body.classList.remove('light-theme', 'dark-theme'); documentRef.body.classList.add(`${theme}-theme`); } + presentationState?.set?.({ theme }); documentRef.body.removeAttribute('data-theme-pending'); + }; + const applyPresentation = async (mode, { persist = false, preserveScroll = true, notify: shouldNotify = false, source = 'unknown' } = {}) => { + if (disposed) return { success: false, mode: normalizePresentation(settingsOwner.get().chatPresentationMode), error: new Error('Theme owner disposed') }; + const normalized = normalizePresentation(mode); const previous = normalizePresentation(settingsOwner.get().chatPresentationMode); const anchor = preserveScroll ? captureAnchor?.() : null; + documentRef.body?.classList.remove(...MODES.map(value => `chat-presentation-${value}`)); documentRef.body?.classList.add(`chat-presentation-${normalized}`); + settingsOwner.update('chatPresentationMode', normalized); syncControls?.(normalized); + if (pretextBridge?.setPresentationMode) pretextBridge.setPresentationMode(normalized); else pretextBridge?.clearAll?.(); + refreshLayout?.(); + if (anchor) scheduleFrame?.(() => scheduleFrame?.(() => restoreAnchor?.(anchor))); + if (!persist || normalized === previous) return { success: true, mode: normalized }; + try { + const result = await saveSettings?.({ chatPresentationMode: normalized }); if (!result?.success) throw new Error(result?.error || '设置保存失败'); + if (shouldNotify) notify?.('聊天显示模式已切换。', 'success'); return { success: true, mode: normalized, source }; + } catch (error) { + await applyPresentation(previous, { persist: false, preserveScroll: true, source: 'rollback' }); notify?.(`聊天显示模式保存失败:${error.message}`, 'error'); return { success: false, mode: previous, error }; + } + }; + const dispose = () => { disposed = true; }; + return Object.freeze({ normalizePresentation, applyInitialTheme, applyPresentation, dispose }); +} diff --git a/modules/renderer/messageContextMenu.js b/modules/renderer/messageContextMenu.js index 4806dddda..58e487a7f 100644 --- a/modules/renderer/messageContextMenu.js +++ b/modules/renderer/messageContextMenu.js @@ -1,41 +1,56 @@ -// modules/renderer/messageContextMenu.js - -let mainRefs = {}; -let contextMenuDependencies = {}; +// modules/renderer/messageContextMenu.js + +/** Creates one context-menu state owner for one MessageRenderer instance. */ +export function createMessageContextMenu() { +let mainRefs = {}; +let contextMenuDependencies = {}; +let ownerDocument = null; +let ownerWindow = null; + +async function cancelOwnedStream(messageId, reason) { + const outcome = await contextMenuDependencies.cancelStream?.(messageId, reason); + if (!outcome) { + contextMenuDependencies.discardStreamingMessage?.(messageId); + await contextMenuDependencies.removeMessageById?.(messageId, true); + } + return outcome; +} /** * Initializes the context menu module with necessary references and dependencies. * @param {object} refs - Core references (electronAPI, uiHelper, etc.). * @param {object} dependencies - Functions from other modules (e.g., from messageRenderer). */ -function initializeContextMenu(refs, dependencies) { - mainRefs = refs; - contextMenuDependencies = dependencies; - - // 防止重复初始化时叠加全局点击监听,造成右键菜单关闭逻辑重复触发 - document.removeEventListener('click', closeContextMenuOnClickOutside, true); - document.addEventListener('click', closeContextMenuOnClickOutside, true); -} - -function closeContextMenu() { - const existingMenu = document.getElementById('chatContextMenu'); +function initializeContextMenu(refs, dependencies) { + ownerDocument?.removeEventListener('click', closeContextMenuOnClickOutside, true); + mainRefs = refs; + contextMenuDependencies = dependencies; + ownerDocument = refs.chatMessagesDiv?.ownerDocument || document; + ownerWindow = ownerDocument.defaultView || window; + + // 防止重复初始化时叠加全局点击监听,造成右键菜单关闭逻辑重复触发 + ownerDocument.addEventListener('click', closeContextMenuOnClickOutside, true); +} + +function closeContextMenu() { + const existingMenu = ownerDocument?.getElementById('chatContextMenu'); if (existingMenu) { existingMenu.remove(); } } // Separate closer for topic context menu to avoid interference -function closeTopicContextMenu() { - const existingMenu = document.getElementById('topicContextMenu'); +function closeTopicContextMenu() { + const existingMenu = ownerDocument?.getElementById('topicContextMenu'); if (existingMenu) existingMenu.remove(); } function closeContextMenuOnClickOutside(event) { - const menu = document.getElementById('chatContextMenu'); + const menu = ownerDocument?.getElementById('chatContextMenu'); if (menu && !menu.contains(event.target)) { closeContextMenu(); } - const topicMenu = document.getElementById('topicContextMenu'); + const topicMenu = ownerDocument?.getElementById('topicContextMenu'); if (topicMenu && !topicMenu.contains(event.target)) { closeTopicContextMenu(); } @@ -50,7 +65,7 @@ function showContextMenu(event, messageItem, message) { const currentSelectedItemVal = mainRefs.currentSelectedItemRef.get(); const currentTopicIdVal = mainRefs.currentTopicIdRef.get(); - const menu = document.createElement('div'); + const menu = ownerDocument.createElement('div'); menu.id = 'chatContextMenu'; menu.classList.add('context-menu'); @@ -58,7 +73,7 @@ function showContextMenu(event, messageItem, message) { const isError = message.finishReason === 'error'; if (isThinkingOrStreaming) { - const interruptOption = document.createElement('div'); + const interruptOption = ownerDocument.createElement('div'); interruptOption.classList.add('context-menu-item', 'danger-item'); interruptOption.innerHTML = ` 中止回复`; interruptOption.onclick = async () => { @@ -78,9 +93,7 @@ function showContextMenu(event, messageItem, message) { } else { uiHelper.showToastNotification(`群聊中止失败: ${result.error}`, "error"); // 作为后备,在前端直接停止渲染 - if (contextMenuDependencies.finalizeStreamedMessage) { - contextMenuDependencies.finalizeStreamedMessage(activeMessageId, 'cancelled_by_user'); - } + await cancelOwnedStream(activeMessageId, result.error || 'group-interrupt-failed'); } } else { console.error("[ContextMenu] electronAPI.interruptGroupRequest is not available."); @@ -98,9 +111,7 @@ function showContextMenu(event, messageItem, message) { uiHelper.showToastNotification(`中止失败: ${result.error}`, "error"); // 中止失败时手动finalize消息 - if (contextMenuDependencies.finalizeStreamedMessage) { - contextMenuDependencies.finalizeStreamedMessage(activeMessageId, 'cancelled_by_user'); - } + await cancelOwnedStream(activeMessageId, result.error || 'agent-interrupt-failed'); // Flowlock 不在此处直接恢复。中止/错误后的重试由对应 Agent Session // 基于 messageId/context 的最终事件统一调度,避免读取其他 Agent 的输入框。 @@ -108,9 +119,7 @@ function showContextMenu(event, messageItem, message) { } else { console.error("[ContextMenu] Interrupt handler not available. Manually cancelling."); uiHelper.showToastNotification("无法发送中止信号,已在本地取消。", "warning"); - if (contextMenuDependencies.finalizeStreamedMessage) { - contextMenuDependencies.finalizeStreamedMessage(activeMessageId, 'cancelled_by_user'); - } + await cancelOwnedStream(activeMessageId, 'interrupt-handler-unavailable'); } } }; @@ -123,7 +132,7 @@ function showContextMenu(event, messageItem, message) { const textarea = isEditing ? messageItem.querySelector('.message-edit-textarea') : null; if (!isEditing) { - const editOption = document.createElement('div'); + const editOption = ownerDocument.createElement('div'); editOption.classList.add('context-menu-item'); editOption.innerHTML = ` 编辑消息`; editOption.onclick = () => { @@ -133,7 +142,7 @@ function showContextMenu(event, messageItem, message) { menu.appendChild(editOption); } - const copyOption = document.createElement('div'); + const copyOption = ownerDocument.createElement('div'); copyOption.classList.add('context-menu-item'); copyOption.innerHTML = ` 复制文本`; copyOption.onclick = () => { @@ -168,15 +177,15 @@ function showContextMenu(event, messageItem, message) { menu.appendChild(copyOption); if (isEditing && textarea) { - const cutOption = document.createElement('div'); + const cutOption = ownerDocument.createElement('div'); cutOption.classList.add('context-menu-item'); cutOption.innerHTML = ` 剪切文本`; cutOption.onclick = () => { - textarea.focus(); document.execCommand('cut'); closeContextMenu(); + textarea.focus(); ownerDocument.execCommand('cut'); closeContextMenu(); }; menu.appendChild(cutOption); - const pasteOption = document.createElement('div'); + const pasteOption = ownerDocument.createElement('div'); pasteOption.classList.add('context-menu-item'); pasteOption.innerHTML = ` 粘贴文本`; pasteOption.onclick = async () => { @@ -196,7 +205,7 @@ function showContextMenu(event, messageItem, message) { } if (currentSelectedItemVal.type === 'agent' || currentSelectedItemVal.type === 'group') { - const createBranchOption = document.createElement('div'); + const createBranchOption = ownerDocument.createElement('div'); createBranchOption.classList.add('context-menu-item'); createBranchOption.innerHTML = ` 创建分支`; createBranchOption.onclick = () => { @@ -208,7 +217,7 @@ function showContextMenu(event, messageItem, message) { menu.appendChild(createBranchOption); } - const forwardOption = document.createElement('div'); + const forwardOption = ownerDocument.createElement('div'); forwardOption.classList.add('context-menu-item'); forwardOption.innerHTML = ` 转发消息`; forwardOption.onclick = () => { @@ -221,13 +230,13 @@ function showContextMenu(event, messageItem, message) { // Add "Read Aloud" option for assistant messages if (message.role === 'assistant') { - const readAloudOption = document.createElement('div'); + const readAloudOption = ownerDocument.createElement('div'); readAloudOption.classList.add('context-menu-item', 'context-menu-item-speak'); readAloudOption.innerHTML = ` 朗读气泡`; readAloudOption.onclick = async () => { // **关键修复:在发送请求前,确保音频上下文已激活** - if (typeof window.ensureAudioContext === 'function') { - window.ensureAudioContext(); + if (typeof contextMenuDependencies.ensureAudioContext === 'function') { + contextMenuDependencies.ensureAudioContext(); } const agentId = message.agentId || currentSelectedItemVal.id; @@ -281,7 +290,7 @@ function showContextMenu(event, messageItem, message) { menu.appendChild(readAloudOption); } - const readModeOption = document.createElement('div'); + const readModeOption = ownerDocument.createElement('div'); readModeOption.classList.add('context-menu-item', 'info-item'); readModeOption.innerHTML = ` 阅读模式`; readModeOption.onclick = async () => { // Make it async @@ -311,7 +320,7 @@ function showContextMenu(event, messageItem, message) { const contentString = (typeof rawContent === 'string') ? rawContent : (rawContent?.text || ''); const windowTitle = `阅读: ${message.id.substring(0, 10)}...`; - const currentTheme = document.body.classList.contains('light-theme') ? 'light' : 'dark'; + const currentTheme = ownerDocument.body.classList.contains('light-theme') ? 'light' : 'dark'; if (electronAPI && typeof electronAPI.openTextInNewWindow === 'function') { electronAPI.openTextInNewWindow(contentString, windowTitle, currentTheme); @@ -329,7 +338,7 @@ function showContextMenu(event, messageItem, message) { }; menu.appendChild(readModeOption); - const deleteOption = document.createElement('div'); + const deleteOption = ownerDocument.createElement('div'); deleteOption.classList.add('context-menu-item', 'danger-item'); deleteOption.innerHTML = ` 删除消息`; deleteOption.onclick = async () => { @@ -350,7 +359,7 @@ function showContextMenu(event, messageItem, message) { // Regenerate option should be here to maintain order if (message.role === 'assistant' && !message.isGroupMessage && currentSelectedItemVal.type === 'agent') { - const regenerateOption = document.createElement('div'); + const regenerateOption = ownerDocument.createElement('div'); regenerateOption.classList.add('context-menu-item', 'regenerate-text'); regenerateOption.innerHTML = ` 重新回复`; regenerateOption.onclick = () => { @@ -362,7 +371,7 @@ function showContextMenu(event, messageItem, message) { // 新增:群聊中的“重新回复”功能 if (message.role === 'assistant' && message.isGroupMessage) { - const redoGroupOption = document.createElement('div'); + const redoGroupOption = ownerDocument.createElement('div'); redoGroupOption.classList.add('context-menu-item', 'regenerate-text'); redoGroupOption.innerHTML = ` 重新回复`; redoGroupOption.onclick = () => { @@ -386,12 +395,12 @@ function showContextMenu(event, messageItem, message) { menu.style.visibility = 'hidden'; menu.style.position = 'absolute'; - document.body.appendChild(menu); + ownerDocument.body.appendChild(menu); const menuWidth = menu.offsetWidth; const menuHeight = menu.offsetHeight; - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; + const windowWidth = ownerWindow.innerWidth; + const windowHeight = ownerWindow.innerHeight; let top = event.clientY; let left = event.clientX; @@ -467,7 +476,7 @@ function toggleEditMode(messageItem, message) { messageItem.classList.add('message-item-editing'); - const textarea = document.createElement('textarea'); + const textarea = ownerDocument.createElement('textarea'); textarea.classList.add('message-edit-textarea'); let textForEditing = ""; @@ -482,10 +491,10 @@ function toggleEditMode(messageItem, message) { textarea.style.minHeight = `${Math.max(originalContentHeight, 50)}px`; textarea.style.width = '100%'; - const controlsDiv = document.createElement('div'); + const controlsDiv = ownerDocument.createElement('div'); controlsDiv.classList.add('message-edit-controls'); - const saveButton = document.createElement('button'); + const saveButton = ownerDocument.createElement('button'); saveButton.innerHTML = ` 保存`; saveButton.onclick = async () => { // 🔧 关键修复:添加防御性编程和错误处理 @@ -543,17 +552,13 @@ function toggleEditMode(messageItem, message) { try { if (currentSelectedItemVal.id && currentTopicIdVal) { - let saveResult; - if (currentSelectedItemVal.type === 'agent') { - saveResult = await electronAPI.saveChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); - } else if (currentSelectedItemVal.type === 'group' && electronAPI.saveGroupChatHistory) { - saveResult = await electronAPI.saveGroupChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); - } - - // 🔧 检查保存结果 - if (saveResult && !saveResult.success) { - throw new Error(saveResult.error || '保存失败'); - } + if (!mainRefs.historyMutationAuthority) throw new Error('History mutation authority is required for message edits'); + await mainRefs.historyMutationAuthority.replace({ + itemId: currentSelectedItemVal.id, + itemType: currentSelectedItemVal.type, + topicId: currentTopicIdVal, + category: 'message-edit', + }, currentChatHistoryArray); } } catch (error) { console.error('[EditMode] Save failed, rolling back:', error); @@ -614,7 +619,7 @@ function toggleEditMode(messageItem, message) { toggleEditMode(messageItem, message); }; - const cancelButton = document.createElement('button'); + const cancelButton = ownerDocument.createElement('button'); cancelButton.innerHTML = ` 取消`; cancelButton.onclick = () => { toggleEditMode(messageItem, message); @@ -660,9 +665,11 @@ function attachTimestampMetaToVcpMessage(vcpMessage, historyMessage) { async function handleRegenerateResponse(originalAssistantMessage) { const { electronAPI, uiHelper } = mainRefs; const currentChatHistoryArray = mainRefs.currentChatHistoryRef.get(); - const currentSelectedItemVal = mainRefs.currentSelectedItemRef.get(); - const currentTopicIdVal = mainRefs.currentTopicIdRef.get(); - const globalSettingsVal = mainRefs.globalSettingsRef.get(); + const currentSelectedItemVal = mainRefs.currentSelectedItemRef.get(); + const currentTopicIdVal = mainRefs.currentTopicIdRef.get(); + const globalSettingsVal = mainRefs.globalSettingsRef.get(); + let streamingRequested = false; + let streamContext = null; if (!currentSelectedItemVal.id || currentSelectedItemVal.type !== 'agent' || !currentTopicIdVal || !originalAssistantMessage || originalAssistantMessage.role !== 'assistant') { uiHelper.showToastNotification("只能为 Agent 的回复进行重新生成。", "warning"); @@ -679,9 +686,15 @@ async function handleRegenerateResponse(originalAssistantMessage) { mainRefs.currentChatHistoryRef.set([...currentChatHistoryArray]); messagesToRemove.forEach(msg => contextMenuDependencies.removeMessageById(msg.id, false)); // false = don't save history again - if (currentSelectedItemVal.id && currentTopicIdVal) { - try { - await electronAPI.saveChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); + if (currentSelectedItemVal.id && currentTopicIdVal) { + try { + if (!mainRefs.historyMutationAuthority) throw new Error('History mutation authority is required for regeneration'); + await mainRefs.historyMutationAuthority.replace({ + itemId: currentSelectedItemVal.id, + itemType: currentSelectedItemVal.type, + topicId: currentTopicIdVal, + category: 'regeneration-truncate', + }, currentChatHistoryArray); } catch (saveError) { console.error("ContextMenu: Failed to save chat history after splice in regenerate:", saveError); } @@ -701,7 +714,7 @@ async function handleRegenerateResponse(originalAssistantMessage) { contextMenuDependencies.renderMessage(regenerationThinkingMessage, false); currentChatHistoryArray.push(regenerationThinkingMessage); mainRefs.currentChatHistoryRef.set([...currentChatHistoryArray]); - window.updateSendButtonState?.(); + ownerWindow.updateSendButtonState?.(); try { const agentConfig = await electronAPI.getAgentConfig(currentSelectedItemVal.id); @@ -723,10 +736,10 @@ async function handleRegenerateResponse(originalAssistantMessage) { // VCPChatTarven (高级回复) - 收集当前生效的规则, // 让"重新回复"与正常发送消息保持完全一致的注入行为 - const tavernRules = (window.TavernManager && typeof window.TavernManager.getActiveRulesForScope === 'function') - ? (window.TavernManager.getActiveRulesForScope('agent') || []) - : []; - const tavernEngine = window.TavernRulesEngine; + const tavernRules = (ownerWindow.TavernManager && typeof ownerWindow.TavernManager.getActiveRulesForScope === 'function') + ? (ownerWindow.TavernManager.getActiveRulesForScope('agent') || []) + : []; + const tavernEngine = ownerWindow.TavernRulesEngine; const messagesForVCP = await Promise.all(historyForRegeneration.map(async (msg, index) => { let vcpImageAttachmentsPayload = []; @@ -988,16 +1001,13 @@ async function handleRegenerateResponse(originalAssistantMessage) { stream: agentConfig.streamOutput === true || String(agentConfig.streamOutput) === 'true' }; - // 【关键修复】如果使用流式输出,先调用 startStreamingMessage - if (modelConfigForVCP.stream) { - contextMenuDependencies.startStreamingMessage({ ...regenerationThinkingMessage, content: "" }); - } - - const context = { - agentId: currentSelectedItemVal.id, - topicId: currentTopicIdVal, - isGroupMessage: false - }; + const context = { + agentId: currentSelectedItemVal.id, + topicId: currentTopicIdVal, + isGroupMessage: false + }; + streamingRequested = modelConfigForVCP.stream; + streamContext = context; const vcpResult = await electronAPI.sendToVCP( globalSettingsVal.vcpServerUrl, @@ -1011,10 +1021,15 @@ async function handleRegenerateResponse(originalAssistantMessage) { if (modelConfigForVCP.stream) { // 如果流启动失败,vcpResult 会包含错误信息 - if (vcpResult.streamError || !vcpResult.streamingStarted) { - let detailedError = vcpResult.error || '未能启动流'; - contextMenuDependencies.finalizeStreamedMessage(regenerationThinkingMessage.id, 'error', `VCP 流错误 (重新生成): ${detailedError}`); - } + if (vcpResult.streamError || !vcpResult.streamingStarted) { + let detailedError = vcpResult.error || '未能启动流'; + contextMenuDependencies.acceptStreamEvent?.({ + type: 'error', + messageId: regenerationThinkingMessage.id, + error: `VCP 流错误 (重新生成): ${detailedError}`, + context, + }); + } } else { // 非流式处理逻辑 - 参考 chatManager.js 的健壮实现 const { response, context } = vcpResult; // 【修复1】正确解构返回结果 @@ -1041,46 +1056,80 @@ async function handleRegenerateResponse(originalAssistantMessage) { }; // 【修复2】采用更健壮的“读-改-写”模式 - const historyForSave = await electronAPI.getChatHistory(context.agentId, context.topicId); - if (historyForSave && !historyForSave.error) { - // 确保历史记录中没有残余的 "thinking" 消息 - const finalHistory = historyForSave.filter(msg => msg.id !== regenerationThinkingMessage.id && !msg.isThinking); - finalHistory.push(assistantMessage); - - await electronAPI.saveChatHistory(context.agentId, context.topicId, finalHistory); - - if (isForActiveChat) { - mainRefs.currentChatHistoryRef.set(finalHistory); - contextMenuDependencies.renderMessage(assistantMessage); - } - } else { - console.error(`[ContextMenu] Regenerate failed to get history for saving:`, historyForSave.error); - if (isForActiveChat) { - contextMenuDependencies.renderMessage({ role: 'system', content: `重新生成失败:无法读取历史记录以保存。`, timestamp: Date.now() }); - } - } + if (!mainRefs.historyMutationAuthority) throw new Error('History mutation authority is required for regeneration'); + const commit = await mainRefs.historyMutationAuthority.mutate({ + itemId: context.agentId, + itemType: context.isGroupMessage ? 'group' : 'agent', + topicId: context.topicId, + category: 'regeneration-terminal', + }, historyForSave => { + const finalHistory = historyForSave.filter(msg => msg.id !== regenerationThinkingMessage.id && !msg.isThinking); + finalHistory.push(assistantMessage); + return finalHistory; + }); + const finalHistory = [...commit.history]; + + if (isForActiveChat) { + mainRefs.currentChatHistoryRef.set(finalHistory); + contextMenuDependencies.renderMessage(assistantMessage); + } } if (isForActiveChat) { uiHelper.scrollToBottom(); } } - } catch (error) { - contextMenuDependencies.finalizeStreamedMessage(regenerationThinkingMessage.id, 'error', `客户端错误 (重新生成): ${error.message}`); - if (currentSelectedItemVal.id && currentTopicIdVal) await electronAPI.saveChatHistory(currentSelectedItemVal.id, currentTopicIdVal, currentChatHistoryArray); - uiHelper.scrollToBottom(); + } catch (error) { + if (streamingRequested && streamContext) { + contextMenuDependencies.acceptStreamEvent?.({ + type: 'error', + messageId: regenerationThinkingMessage.id, + error: `客户端错误 (重新生成): ${error.message}`, + context: streamContext, + }); + } else { + const failedHistory = currentChatHistoryArray.filter(message => message.id !== regenerationThinkingMessage.id); + mainRefs.currentChatHistoryRef.set(failedHistory); + contextMenuDependencies.removeMessageById(regenerationThinkingMessage.id, false); + contextMenuDependencies.renderMessage({ + role: 'system', + content: `客户端错误 (重新生成): ${error.message}`, + timestamp: Date.now(), + }); + if (currentSelectedItemVal.id && currentTopicIdVal && mainRefs.historyMutationAuthority) { + await mainRefs.historyMutationAuthority.replace({ + itemId: currentSelectedItemVal.id, + itemType: currentSelectedItemVal.type, + topicId: currentTopicIdVal, + category: 'regeneration-failure-cleanup', + }, failedHistory); + } + } + uiHelper.scrollToBottom(); } } -function setContextMenuDependencies(newDependencies) { - contextMenuDependencies = { ...contextMenuDependencies, ...newDependencies }; -} - -export { - initializeContextMenu, - showContextMenu, - closeContextMenu, - toggleEditMode, - handleRegenerateResponse, - setContextMenuDependencies -}; +function setContextMenuDependencies(newDependencies) { + contextMenuDependencies = { ...contextMenuDependencies, ...newDependencies }; +} + +function dispose() { + ownerDocument?.removeEventListener('click', closeContextMenuOnClickOutside, true); + closeContextMenu(); + closeTopicContextMenu(); + mainRefs = {}; + contextMenuDependencies = {}; + ownerDocument = null; + ownerWindow = null; +} + +return Object.freeze({ + initializeContextMenu, + showContextMenu, + closeContextMenu, + toggleEditMode, + handleRegenerateResponse, + setContextMenuDependencies, + dispose, +}); +} diff --git a/modules/renderer/middleClickHandler.js b/modules/renderer/middleClickHandler.js index 8cb0f1f09..8a09493b1 100644 --- a/modules/renderer/middleClickHandler.js +++ b/modules/renderer/middleClickHandler.js @@ -1,7 +1,12 @@ -// modules/renderer/middleClickHandler.js - -let mainRendererReferences = {}; -let callbacks = {}; +// modules/renderer/middleClickHandler.js + +/** Creates one middle-click interaction owner for one MessageRenderer instance. */ +export function createMiddleClickHandler() { +let mainRendererReferences = {}; +let callbacks = {}; +let unloadHandler = null; +let ownerDocument = null; +let ownerWindow = null; // --- State Variables --- let middleClickGrid = null; @@ -189,9 +194,9 @@ function canPerformMiddleClickAction(message, messageItem) { // 检查消息是否在streamManager中被标记为已完成 let isStreamManagerFinalized = false; - if (window.streamManager && typeof window.streamManager.isMessageInitialized === 'function') { - // 如果消息不在streamManager中跟踪,说明已经完成 - isStreamManagerFinalized = !window.streamManager.isMessageInitialized(messageId); + if (callbacks.streamManager && typeof callbacks.streamManager.isMessageInitialized === 'function') { + // 如果消息不在streamManager中跟踪,说明已经完成 + isStreamManagerFinalized = !callbacks.streamManager.isMessageInitialized(messageId); } // 检查消息是否有完成理由(表示已完成) @@ -223,7 +228,7 @@ function canPerformMiddleClickAction(message, messageItem) { * @param {Object} message - The message object * @param {Object} globalSettings - The global settings object */ -function startAdvancedMiddleClickTimer(event, messageItem, message, globalSettings) { +function startAdvancedMiddleClickTimer(event, messageItem, message, globalSettings) { // 首先检查气泡是否处于完成状态 if (!canPerformMiddleClickAction(message, messageItem)) { console.log(`[AdvancedMiddleClick] Ignoring advanced middle click on incomplete message: ${message?.id}`); @@ -307,18 +312,18 @@ function startAdvancedMiddleClickTimer(event, messageItem, message, globalSettin } cleanup(); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); - document.removeEventListener('mousemove', handleMouseMove); + ownerDocument.removeEventListener('mouseup', handleMouseUp); + ownerDocument.removeEventListener('mouseleave', handleMouseLeave); + ownerDocument.removeEventListener('mousemove', handleMouseMove); } }; const handleMouseLeave = () => { console.log('[AdvancedMiddleClick] Mouse left element - cancelling'); cleanup(); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); - document.removeEventListener('mousemove', handleMouseMove); + ownerDocument.removeEventListener('mouseup', handleMouseUp); + ownerDocument.removeEventListener('mouseleave', handleMouseLeave); + ownerDocument.removeEventListener('mousemove', handleMouseMove); }; const handleMouseMove = (e) => { @@ -334,7 +339,7 @@ function startAdvancedMiddleClickTimer(event, messageItem, message, globalSettin console.log(`[AdvancedMiddleClick] Showing grid after ${delay}ms delay`); isAdvancedModeActive = true; showMiddleClickGrid(event.clientX, event.clientY, messageItem, message); - document.addEventListener('mousemove', handleMouseMove); + ownerDocument.addEventListener('mousemove', handleMouseMove); updateGridSelection(event.clientX, event.clientY); @@ -346,16 +351,17 @@ function startAdvancedMiddleClickTimer(event, messageItem, message, globalSettin }, delay); // Add immediate mouseup listener for quick release - document.addEventListener('mouseup', handleMouseUp); - document.addEventListener('mouseleave', handleMouseLeave); + ownerDocument.addEventListener('mouseup', handleMouseUp); + ownerDocument.addEventListener('mouseleave', handleMouseLeave); // Store the timer for potential cleanup activeMiddleClickTimers.set(timerId, { cleanup, handleMouseUp, - handleMouseLeave, - handleMouseMove, - timeoutId + handleMouseLeave, + handleMouseMove, + ownerDocument, + timeoutId }); } @@ -412,32 +418,33 @@ function startMiddleClickTimer(event, messageItem, message, quickAction) { } cleanup(); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); + ownerDocument.removeEventListener('mouseup', handleMouseUp); + ownerDocument.removeEventListener('mouseleave', handleMouseLeave); } }; const handleMouseLeave = () => { console.log('[MiddleClick] Cancelled - mouse left element'); cleanup(); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); + ownerDocument.removeEventListener('mouseup', handleMouseUp); + ownerDocument.removeEventListener('mouseleave', handleMouseLeave); }; // Add event listeners to document to catch mouseup even if mouse moves away - document.addEventListener('mouseup', handleMouseUp); - document.addEventListener('mouseleave', handleMouseLeave); + ownerDocument.addEventListener('mouseup', handleMouseUp); + ownerDocument.addEventListener('mouseleave', handleMouseLeave); // Store the timer for potential cleanup activeMiddleClickTimers.set(timerId, { - cleanup, - handleMouseUp, - handleMouseLeave, - timeoutId: setTimeout(() => { + cleanup, + handleMouseUp, + handleMouseLeave, + ownerDocument, + timeoutId: setTimeout(() => { console.log('[MiddleClick] Cancelled - 1 second timeout reached'); cleanup(); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); + ownerDocument.removeEventListener('mouseup', handleMouseUp); + ownerDocument.removeEventListener('mouseleave', handleMouseLeave); }, 1000) }); } @@ -567,10 +574,8 @@ async function handleMiddleClickQuickAction(event, messageItem, message, quickAc // Now proceed with edit mode try { - if (typeof window.toggleEditMode === 'function') { - window.toggleEditMode(messageItem, message); - } else if (window.messageContextMenu && typeof window.messageContextMenu.toggleEditMode === 'function') { - window.messageContextMenu.toggleEditMode(messageItem, message); + if (typeof callbacks.toggleEditMode === 'function') { + callbacks.toggleEditMode(messageItem, message); } else { uiHelper.showToastNotification("编辑功能暂时不可用", "warning"); } @@ -633,8 +638,8 @@ async function handleMiddleClickQuickAction(event, messageItem, message, quickAc case 'forward': // 转发消息 - 执行与右键菜单完全相同的功能 - if (typeof window.showForwardModal === 'function') { - window.showForwardModal(message); + if (typeof callbacks.showForwardModal === 'function') { + callbacks.showForwardModal(message); uiHelper.showToastNotification("已打开转发对话框", "success"); } else { uiHelper.showToastNotification("转发功能暂时不可用", "warning"); @@ -645,8 +650,8 @@ async function handleMiddleClickQuickAction(event, messageItem, message, quickAc // 朗读气泡 if (message.role === 'assistant') { // Ensure audio context is activated - if (typeof window.ensureAudioContext === 'function') { - window.ensureAudioContext(); + if (typeof callbacks.ensureAudioContext === 'function') { + callbacks.ensureAudioContext(); } const agentId = message.agentId || currentSelectedItemVal.id; @@ -779,8 +784,8 @@ async function handleMiddleClickQuickAction(event, messageItem, message, quickAc } } else { // 非群聊重新回复逻辑(原有逻辑) - if (window.messageContextMenu && typeof window.messageContextMenu.handleRegenerateResponse === 'function') { - window.messageContextMenu.handleRegenerateResponse(message); + if (typeof callbacks.handleRegenerateResponse === 'function') { + callbacks.handleRegenerateResponse(message); uiHelper.showToastNotification("已开始重新生成回复", "success"); } else { uiHelper.showToastNotification("重新回复功能暂时不可用", "warning"); @@ -1092,13 +1097,16 @@ function showTestMiddleClickGrid() { */ function cleanupAllMiddleClickTimers() { console.log(`[MiddleClick] Cleaning up ${activeMiddleClickTimers.size} active timers`); - for (const [timerId, timerData] of activeMiddleClickTimers.entries()) { - if (timerData.timeoutId) { - clearTimeout(timerData.timeoutId); - } - if (timerData.cleanup) { - timerData.cleanup(); - } + for (const [timerId, timerData] of activeMiddleClickTimers.entries()) { + if (timerData.timeoutId) { + clearTimeout(timerData.timeoutId); + } + timerData.ownerDocument?.removeEventListener('mouseup', timerData.handleMouseUp); + timerData.ownerDocument?.removeEventListener('mouseleave', timerData.handleMouseLeave); + timerData.ownerDocument?.removeEventListener('mousemove', timerData.handleMouseMove); + if (timerData.cleanup) { + timerData.cleanup(); + } } activeMiddleClickTimers.clear(); @@ -1113,8 +1121,7 @@ function cleanupAllMiddleClickTimers() { freezeGridCancellation = false; // 恢复原始的 showToastNotification 函数 - if (mainRendererReferences.uiHelper.showToastNotification && - mainRendererReferences.uiHelper.showToastNotification.tempShowToast) { + if (mainRendererReferences.uiHelper?.showToastNotification?.tempShowToast) { mainRendererReferences.uiHelper.showToastNotification = mainRendererReferences.uiHelper.showToastNotification.originalShowToast; } @@ -1128,34 +1135,48 @@ function getMiddleClickState() { advancedModeActive: isAdvancedModeActive, isDeletingMessage: isDeletingMessage, freezeGridCancellation: freezeGridCancellation, - toastFunctionModified: !!(mainRendererReferences.uiHelper.showToastNotification && - mainRendererReferences.uiHelper.showToastNotification.tempShowToast) + toastFunctionModified: !!mainRendererReferences.uiHelper?.showToastNotification?.tempShowToast }; } -function initialize(refs, cb) { - mainRendererReferences = refs; - callbacks = cb; - // Add cleanup on page unload - window.addEventListener('beforeunload', () => { - cleanupAllMiddleClickTimers(); +function initialize(refs, cb) { + if (unloadHandler) ownerWindow?.removeEventListener('beforeunload', unloadHandler); + mainRendererReferences = refs; + callbacks = cb; + ownerDocument = refs.chatMessagesDiv?.ownerDocument || document; + ownerWindow = ownerDocument.defaultView || window; + // Add cleanup on page unload + unloadHandler = () => { + cleanupAllMiddleClickTimers(); isDeletingMessage = false; freezeGridCancellation = false; // 恢复原始的 showToastNotification 函数 - if (mainRendererReferences.uiHelper.showToastNotification && - mainRendererReferences.uiHelper.showToastNotification.tempShowToast) { + if (mainRendererReferences.uiHelper?.showToastNotification?.tempShowToast) { mainRendererReferences.uiHelper.showToastNotification = mainRendererReferences.uiHelper.showToastNotification.originalShowToast; } - }); -} - -export { - initialize, - startMiddleClickTimer, - startAdvancedMiddleClickTimer, - cleanupAllMiddleClickTimers, - showTestMiddleClickGrid, - getMiddleClickState -}; + }; + ownerWindow.addEventListener('beforeunload', unloadHandler); +} + +function dispose() { + if (unloadHandler) ownerWindow?.removeEventListener('beforeunload', unloadHandler); + unloadHandler = null; + cleanupAllMiddleClickTimers(); + mainRendererReferences = {}; + callbacks = {}; + ownerDocument = null; + ownerWindow = null; +} + +return Object.freeze({ + initialize, + startMiddleClickTimer, + startAdvancedMiddleClickTimer, + cleanupAllMiddleClickTimers, + showTestMiddleClickGrid, + getMiddleClickState, + dispose, +}); +} diff --git a/modules/renderer/nonStreamingEventConsumer.js b/modules/renderer/nonStreamingEventConsumer.js new file mode 100644 index 000000000..e13f4fc92 --- /dev/null +++ b/modules/renderer/nonStreamingEventConsumer.js @@ -0,0 +1,27 @@ +/** Consumes non-stream events whose producer has already committed history. */ +export function createNonStreamingEventConsumer({ renderTarget, messageRenderer, viewAuthority, onUnhandled = console.warn } = {}) { + if (!renderTarget || !messageRenderer) throw new TypeError('Non-streaming consumer requires an owning render target'); + let disposed = false; + return Object.freeze({ + async consume(event = {}) { + if (disposed) return false; + const type = String(event.type || ''); + const relevant = viewAuthority?.isCurrent?.(event.context) === true; + if (type === 'full_response') { + if (relevant) await messageRenderer.renderFullMessageProjection?.( + event.messageId, event.fullResponse || '', event.context?.agentName, event.context?.agentId, renderTarget.root + ); + return true; + } + if (type === 'remove_message') { + if (relevant) await renderTarget.removeMessage(event.messageId, false); + return true; + } + if (type === 'no_ai_response') return true; + if (!relevant) return false; + if (type) onUnhandled(`[NonStreamingEventConsumer] Unhandled event: ${type}`, event); + return false; + }, + dispose() { disposed = true; }, + }); +} diff --git a/modules/renderer/ownedPreloadSubscription.js b/modules/renderer/ownedPreloadSubscription.js new file mode 100644 index 000000000..97c5c1b5f --- /dev/null +++ b/modules/renderer/ownedPreloadSubscription.js @@ -0,0 +1,38 @@ +/** Owns one preload subscription and forwards producer payloads to a Surface consumer. */ +export function createOwnedPreloadSubscription({ subscribe, consume, reportError = console.error } = {}) { + if (typeof subscribe !== 'function') throw new TypeError('Owned preload subscription requires subscribe'); + if (typeof consume !== 'function') throw new TypeError('Owned preload subscription requires consume'); + let disposed = false; + const tasks = new Set(); + const abortController = new AbortController(); + const lifecycle = Object.freeze({ + signal: abortController.signal, + isActive: () => !disposed, + }); + const disposer = subscribe(payload => { + if (disposed) return; + let result; + try { result = consume(payload, lifecycle); } + catch (error) { reportError('[OwnedPreloadSubscription] consumer failed', error); return undefined; } + if (!result || typeof result.then !== 'function') return result; + const task = Promise.resolve(result) + .catch(error => { reportError('[OwnedPreloadSubscription] consumer failed', error); }) + .finally(() => tasks.delete(task)); + tasks.add(task); + return task; + }); + return Object.freeze({ + async dispose() { + if (disposed) return; + disposed = true; + abortController.abort(); + try { + if (typeof disposer === 'function') await disposer(); + else await disposer?.dispose?.(); + } catch (error) { + reportError('[OwnedPreloadSubscription] unsubscribe failed', error); + } + await Promise.allSettled([...tasks]); + }, + }); +} diff --git a/modules/renderer/renderDependencies.js b/modules/renderer/renderDependencies.js new file mode 100644 index 000000000..b804bf30c --- /dev/null +++ b/modules/renderer/renderDependencies.js @@ -0,0 +1,101 @@ +const requireFunction = (value, label) => { + if (typeof value !== 'function') throw new TypeError(`RenderDependencies requires ${label}`); + return value; +}; +const requireRef = (value, label, writable = false) => { + if (!value || typeof value.get !== 'function' || (writable && typeof value.set !== 'function')) { + throw new TypeError(`RenderDependencies requires ${label} ${writable ? 'read/write' : 'read'} ref`); + } + return value; +}; + +/** Explicit capability closure for the legacy DOM adapter during D4 migration. */ +export function createRenderDependencies(input = {}) { + const root = input.chatMessagesDiv; + if (!root || typeof root.querySelector !== 'function' || typeof root.addEventListener !== 'function') { + throw new TypeError('RenderDependencies requires a DOM root capability'); + } + if (!input.electronAPI || typeof input.electronAPI !== 'object') { + throw new TypeError('RenderDependencies requires an Electron transport capability'); + } + if (!input.chatRepository || typeof input.chatRepository.saveHistory !== 'function') { + throw new TypeError('RenderDependencies requires a chat repository capability'); + } + if (!input.historyMutationAuthority || typeof input.historyMutationAuthority.replace !== 'function') { + throw new TypeError('RenderDependencies requires a history mutation authority'); + } + if (!input.transientStreamHistory || typeof input.transientStreamHistory.prepare !== 'function' || typeof input.transientStreamHistory.finalize !== 'function') { + throw new TypeError('RenderDependencies requires a transient stream history provider'); + } + if (!input.markedInstance || typeof input.markedInstance.parse !== 'function') { + throw new TypeError('RenderDependencies requires a Markdown parser capability'); + } + if (!input.uiHelper || typeof input.uiHelper.scrollToBottom !== 'function') { + throw new TypeError('RenderDependencies requires feedback/navigation capabilities'); + } + + const state = Object.freeze({ + history: requireRef(input.currentChatHistoryRef, 'history', true), + selection: requireRef(input.currentSelectedItemRef, 'selection', true), + topic: requireRef(input.currentTopicIdRef, 'topic', true), + settings: requireRef(input.globalSettingsRef, 'settings', true), + }); + const commands = Object.freeze({ + summarizeTopic: requireFunction(input.summarizeTopicFromMessages, 'summarizeTopic'), + createBranch: requireFunction(input.handleCreateBranch, 'createBranch'), + }); + const providedMessageCommands = input.messageCommands || {}; + const messageCommands = Object.freeze({ + processFilesData: providedMessageCommands.processFilesData || null, + addAttachmentsToMessage: providedMessageCommands.addAttachmentsToMessage || null, + removeAttachmentFromMessage: providedMessageCommands.removeAttachmentFromMessage || null, + syncNextUiEmptyStateWithMessages: providedMessageCommands.syncNextUiEmptyStateWithMessages || null, + handleSendMessage: providedMessageCommands.handleSendMessage || null, + updateSendButtonState: providedMessageCommands.updateSendButtonState || null, + }); + const ownerDocument = root.ownerDocument; + const ownerWindow = ownerDocument?.defaultView; + if (!ownerDocument || !ownerWindow) throw new TypeError('RenderDependencies requires a live DOM realm'); + const closure = { + root, + state, + transport: input.electronAPI, + repository: input.chatRepository, + historyMutations: input.historyMutationAuthority, + transientStreamHistory: input.transientStreamHistory, + markdown: input.markedInstance, + feedback: input.uiHelper, + commands, + messageCommands, + interrupt: input.interruptHandler || null, + chatDomRenderer: input.chatDomRenderer || null, + document: ownerDocument, + realm: ownerWindow, + morphdom: input.morphdom || null, + pretextBridge: input.pretextBridge || null, + flowlockProtocol: input.flowlockProtocol || null, + + // Flat aliases are temporary D4 migration adapters, not public globals. + chatMessagesDiv: root, + currentChatHistoryRef: state.history, + currentSelectedItemRef: state.selection, + currentTopicIdRef: state.topic, + globalSettingsRef: state.settings, + electronAPI: input.electronAPI, + chatRepository: input.chatRepository, + historyMutationAuthority: input.historyMutationAuthority, + transientStreamHistory: input.transientStreamHistory, + markedInstance: input.markedInstance, + uiHelper: input.uiHelper, + summarizeTopicFromMessages: commands.summarizeTopic, + handleCreateBranch: commands.createBranch, + interruptHandler: input.interruptHandler || null, + messageCommands, + document: ownerDocument, + realm: ownerWindow, + morphdom: input.morphdom || null, + pretextBridge: input.pretextBridge || null, + flowlockProtocol: input.flowlockProtocol || null, + }; + return Object.freeze(closure); +} diff --git a/modules/renderer/renderSessionAuthority.js b/modules/renderer/renderSessionAuthority.js new file mode 100644 index 000000000..6f59a6874 --- /dev/null +++ b/modules/renderer/renderSessionAuthority.js @@ -0,0 +1,30 @@ +/** + * Owns progressive-render generations per Surface root. A reload or dispose on + * one root must never revoke delayed work owned by another root. + */ +export function createRenderSessionAuthority({ resolveDefaultRoot = () => null } = {}) { + const generations = new WeakMap(); + let detachedGeneration = 0; + + const resolveRoot = root => root || resolveDefaultRoot() || null; + + return Object.freeze({ + invalidate(root = null) { + const ownerRoot = resolveRoot(root); + if (!ownerRoot) return Object.freeze({ root: null, generation: ++detachedGeneration }); + const generation = (generations.get(ownerRoot) || 0) + 1; + generations.set(ownerRoot, generation); + return Object.freeze({ root: ownerRoot, generation }); + }, + capture(root = null) { + const ownerRoot = resolveRoot(root); + if (!ownerRoot) return Object.freeze({ root: null, generation: detachedGeneration }); + return Object.freeze({ root: ownerRoot, generation: generations.get(ownerRoot) || 0 }); + }, + isActive(session) { + if (!session) return false; + if (!session.root) return session.generation === detachedGeneration; + return session.generation === (generations.get(session.root) || 0); + }, + }); +} diff --git a/modules/renderer/streamManager.js b/modules/renderer/streamManager.js index 0e40661f7..8d0f8d39a 100644 --- a/modules/renderer/streamManager.js +++ b/modules/renderer/streamManager.js @@ -1,26 +1,22 @@ // modules/renderer/streamManager.js -import { formatMessageTimestamp } from './domBuilder.js'; -import { createContentPipeline, PIPELINE_MODES } from './contentPipeline.js'; - -// --- Stream State --- -const streamingChunkQueues = new Map(); // messageId -> array of original chunk strings -const streamingTimers = new Map(); // messageId -> intervalId -const accumulatedStreamText = new Map(); // messageId -> string -const streamSegmentStates = new Map(); // messageId -> { stableCutoff, stableHtml, stableRenderedCutoff, stableBlocks, stableBlockSeq, lastTailText, lastParagraphBoundary } -let activeStreamingMessageId = null; // Track the currently active streaming message -const elementContentLengthCache = new WeakMap(); // 跟踪每个元素的内容长度;WeakMap 避免 morphdom 替换节点后的强引用泄漏 +import { formatMessageTimestamp } from './domBuilder.js'; +import { createContentPipeline, PIPELINE_MODES } from './contentPipeline.js'; +import { createContentRuntime } from '../chat/contentRuntime.js'; +import { createDesktopPushConsumer } from './desktopPushConsumer.js'; +import { createStreamProjectionRuntime } from './streamProjectionRuntime.js'; + +/** Creates one DOM stream projection owner for one renderer Surface. */ +export function createStreamProjection() { +const runtime = createStreamProjectionRuntime(); +const { + messageRuntimeKeys, streamingChunkQueues, streamingTimers, accumulatedStreamText, + streamSegmentStates, activeStreamingMessages, elementContentLengthCache, +} = runtime; +// Renderer-local active stream facts. This is not durable history. +const streamMessageModels = new Map(); const STREAM_CODE_LINE_SWEEP_DURATION_MS = 2400; const STREAM_CODE_MAX_ACTIVE_SWEEPS = 3; -// --- VCPdesktop 流式推送状态 --- -const desktopPushStates = new Map(); // messageId -> { active, widgetId, buffer, tagBuffer, created, pushTimer, lastPushedLength, lastTokenTime, validated } -const DESKTOP_PUSH_START_TAG = '<<<[DESKTOP_PUSH]>>>'; -const DESKTOP_PUSH_END_TAG = '<<<[DESKTOP_PUSH_END]>>>'; -const DESKTOP_PUSH_THROTTLE_MS = 100; // 每100ms推送一次累积内容到桌面画布 -const DESKTOP_PUSH_TIMEOUT_MS = 150000; // 150秒超时:未闭合的推送块自动finalize -const DESKTOP_PUSH_VALID_PREFIXES = ['>>'; const TOOL_REQUEST_END = '<<<[END_TOOL_REQUEST]>>>'; const TOOL_RESULT_START = '[[VCP调用结果信息汇总:'; @@ -167,35 +163,81 @@ function preserveDynamicStreamState(fromEl, toEl) { } } -// --- DOM Cache --- -const messageDomCache = new Map(); // messageId -> { messageItem, contentDiv } - -const scrollThrottleTimers = new Map(); // messageId -> timerId -const SCROLL_THROTTLE_MS = 100; // 100ms 节流 -const viewContextCache = new Map(); // messageId -> boolean (是否为当前视图) -let currentViewSignature = null; // 当前视图的签名 -let globalRenderLoopRunning = false; -const pendingDirectRenderMessages = new Set(); // 非平滑流式:chunk 到达只置脏,由全局 rAF 合帧渲染 - -// 记录延迟清理定时器,方便切换话题时统一清除 -const delayedCleanupTimers = new Map(); // messageId -> timerId - -// --- 新增:预缓冲系统 --- -const preBufferedChunks = new Map(); // messageId -> array of chunks waiting for initialization -const messageInitializationStatus = new Map(); // messageId -> 'pending' | 'ready' | 'finalized' -const pendingFinalizationEvents = new Map(); // messageId -> { finishReason, context, finalPayload } -// Renderer-owned pending entries are deliberately not persisted. They carry -// enough ordering metadata to finalize a stream after its source topic has -// moved to the background without leaving crash-residue in history.json. -const pendingHistoryEntries = new Map(); // messageId -> pending assistant message - -// --- 新增:消息上下文映射 --- -const messageContextMap = new Map(); // messageId -> {agentId, groupId, topicId, isGroupMessage} +// --- Surface-owned runtime projections --- +const { + messageDomCache, scrollThrottleTimers, viewContextCache, + pendingDirectRenderMessages, preBufferedChunks, messageInitializationStatus, + messageInitializationWaiters, messageContextMap, +} = runtime; +const SCROLL_THROTTLE_MS = 100; // 100ms 节流 +let currentViewSignature = null; // 当前视图的签名 +let globalRenderLoopRunning = false; // --- Local Reference Store --- -let refs = {}; -let contentPipeline = null; -let transientCleanupRegistered = false; +let refs = {}; +let contentPipeline = null; +let contentRuntime = null; +let transientCleanupRegistered = false; +let transientCleanupWindow = null; +let desktopPushConsumer = null; +let disposed = false; +const pendingAsyncOperations = new Set(); +const ownedTimeouts = new Set(); +const scheduledAnimationFrames = new Set(); + +function trackAsyncOperation(operation) { + const promise = Promise.resolve(operation); + pendingAsyncOperations.add(promise); + promise.finally(() => pendingAsyncOperations.delete(promise)).catch(() => {}); + return promise; +} + +function scheduleOwnedTimeout(callback, delay = 0) { + const environment = ownerWindow?.() || globalThis; + const set = environment.setTimeout?.bind(environment) || setTimeout; + const clear = environment.clearTimeout?.bind(environment) || clearTimeout; + const record = { id: null, clear }; + record.id = set(() => { + ownedTimeouts.delete(record); + if (!disposed) callback(); + }, delay); + ownedTimeouts.add(record); + return record; +} + +function clearOwnedTimeout(record) { + if (!record) return; + record.clear(record.id); + ownedTimeouts.delete(record); +} + +const ownerDocument = () => refs.document || refs.chatMessagesDiv?.ownerDocument || null; +const ownerWindow = () => refs.window || ownerDocument()?.defaultView || null; + +function scheduleAnimationFrame(callback) { + if (disposed) return null; + const environment = ownerWindow(); + if (!environment) return null; + const entry = { handle: null }; + const invoke = timestamp => { + scheduledAnimationFrames.delete(entry); + if (!disposed) callback(timestamp); + }; + if (typeof environment.requestAnimationFrame === 'function') { + entry.handle = environment.requestAnimationFrame(invoke); + entry.cancel = () => environment.cancelAnimationFrame?.(entry.handle); + } else { + entry.handle = environment.setTimeout(() => invoke(Date.now()), 0); + entry.cancel = () => environment.clearTimeout?.(entry.handle); + } + scheduledAnimationFrames.add(entry); + return entry.handle; +} + +function cancelScheduledAnimationFrames() { + for (const entry of scheduledAnimationFrames) entry.cancel?.(); + scheduledAnimationFrames.clear(); +} // --- Pre-compiled Regular Expressions for Performance --- @@ -203,17 +245,29 @@ let transientCleanupRegistered = false; * Initializes the Stream Manager with necessary dependencies from the main renderer. * @param {object} dependencies - An object containing all required functions and references. */ -export function initStreamManager(dependencies) { - refs = dependencies; +function attachStreamProjection(dependencies) { + if (disposed) throw new Error('StreamProjection is disposed'); + refs = dependencies; + if (!dependencies?.chatMessagesDiv?.querySelector) { + throw new TypeError('StreamProjection requires an owning Surface root'); + } + if (typeof dependencies?.viewAuthority?.isCurrent !== 'function') { + throw new TypeError('StreamProjection requires an explicit view authority'); + } + if (!dependencies.transientStreamHistory || typeof dependencies.transientStreamHistory.prepare !== 'function' || typeof dependencies.transientStreamHistory.finalize !== 'function') { + throw new TypeError('StreamProjection requires an explicit transient history capability'); + } - // App 级兜底扫帚:页面卸载时释放孤儿流的预缓冲、上下文映射、桌面推送 interval 等 transient 状态。 - // 不挂到 clearChat,避免切换话题时误伤同窗口内其他 agent 的后台流式聊天。 - if (!transientCleanupRegistered) { - window.addEventListener('beforeunload', cleanupTransientState); - transientCleanupRegistered = true; + // App 级兜底扫帚:页面卸载时释放孤儿流的预缓冲、上下文映射、桌面推送 interval 等 transient 状态。 + // 不挂到 clearChat,避免切换话题时误伤同窗口内其他 agent 的后台流式聊天。 + if (!transientCleanupRegistered) { + transientCleanupWindow = dependencies.window || dependencies.chatMessagesDiv?.ownerDocument?.defaultView || null; + if (!transientCleanupWindow) throw new TypeError('StreamProjection requires an owning window capability'); + transientCleanupWindow.addEventListener('beforeunload', dispose); + transientCleanupRegistered = true; } - contentPipeline = createContentPipeline({ + contentPipeline = createContentPipeline({ fixEmoticonUrlsInMarkdown: (text) => { if (!text || typeof text !== 'string' || !refs.emoticonUrlFixer) return text; @@ -248,23 +302,27 @@ export function initStreamManager(dependencies) { processedText = refs.ensureSeparatorBetweenImgAndCode(processedText); } return processedText; - } - }); + } + }); + contentRuntime = typeof createContentRuntime === 'function' + ? createContentRuntime({ pipeline: contentPipeline }) + : { + processStream: (text, options = {}) => contentPipeline.process(text, { ...options, mode: PIPELINE_MODES.STREAM_FAST }), + extractChunkText: (chunk) => { + if (chunk?.error === 'json_parse_error') return ''; + return chunk?.choices?.[0]?.delta?.content || chunk?.delta?.content || chunk?.content || (typeof chunk === 'string' ? chunk : '') || (chunk?.raw && !chunk?.error ? chunk.raw : ''); + } + }; // Assume morphdom is passed in dependencies, warn if not present. if (!refs.morphdom) { console.warn('[StreamManager] `morphdom` not provided. Streaming rendering will fall back to inefficient innerHTML updates.'); } - // 监听桌面窗口状态,缓存到本地标志位 - // 这样在流式推送时就不需要每个token都做IPC查询 - if (refs.electronAPI?.onDesktopStatus) { - refs.electronAPI.onDesktopStatus((data) => { - desktopWindowAvailable = !!data.connected; - console.log(`[StreamManager] Desktop window availability changed: ${desktopWindowAvailable}`); - }); - } -} + desktopPushConsumer?.dispose(); + desktopPushConsumer = createDesktopPushConsumer({ electronAPI: refs.electronAPI }); + desktopPushConsumer.start(); +} function shouldEnableSmoothStreaming() { const globalSettings = refs.globalSettingsRef.get(); @@ -281,7 +339,7 @@ function messageIsFinalized(messageId) { * 判断请求是否仍处于等待首块或流式处理中。 * 该 Map 是跨 Agent/话题异步请求的运行态真源,不能只依赖单一的 activeStreamingMessageId。 */ -export function isMessageActive(messageId) { +function isMessageActive(messageId) { const initStatus = messageInitializationStatus.get(messageId); return initStatus === 'pending' || initStatus === 'ready'; } @@ -304,114 +362,21 @@ function getCurrentViewSignature() { /** * 🟢 带缓存的视图检查 */ -function isMessageForCurrentView(context) { - if (!context) return false; - - const newSignature = getCurrentViewSignature(); - - // 如果视图切换了,清空缓存 - if (currentViewSignature !== newSignature) { - currentViewSignature = newSignature; - viewContextCache.clear(); - } - - const currentSelectedItem = refs.currentSelectedItemRef.get(); - const currentTopicId = refs.currentTopicIdRef.get(); - - if (!currentSelectedItem || !currentTopicId) return false; - - const itemId = context.groupId || context.agentId; - return itemId === currentSelectedItem.id && context.topicId === currentTopicId; -} - -async function getHistoryForContext(context) { - const { electronAPI } = refs; - if (!context) return null; - - const { agentId, groupId, topicId, isGroupMessage } = context; - const itemId = groupId || agentId; - - if (!itemId || !topicId) return null; - - try { - const historyResult = isGroupMessage - ? await electronAPI.getGroupChatHistory(itemId, topicId) - : await electronAPI.getChatHistory(itemId, topicId); - - if (historyResult && !historyResult.error) { - return historyResult; - } - } catch (e) { - console.error(`[StreamManager] Failed to get history for context`, context, e); - } - - return null; -} - -// 🟢 历史保存防抖 -const historySaveQueue = new Map(); // context signature -> {context, history, timerId} -const HISTORY_SAVE_DEBOUNCE = 1000; // 1秒防抖 - -async function debouncedSaveHistory(context, history) { - if (!context || context.topicId === 'assistant_chat' || context.topicId?.startsWith('voicechat_')) { - return; // 跳过临时聊天 - } - - const signature = `${context.groupId || context.agentId}-${context.topicId}`; - - // 清除之前的定时器 - const existing = historySaveQueue.get(signature); - if (existing?.timerId) { - clearTimeout(existing.timerId); - } - - // 设置新的防抖定时器 - const timerId = setTimeout(async () => { - const queuedData = historySaveQueue.get(signature); - if (queuedData) { - await saveHistoryForContext(queuedData.context, queuedData.history); - historySaveQueue.delete(signature); - } - }, HISTORY_SAVE_DEBOUNCE); - - // 使用最新的 history 克隆以避免引用问题 - historySaveQueue.set(signature, { context, history: [...history], timerId }); -} - -async function saveHistoryForContext(context, history) { - const { electronAPI } = refs; - if (!context || context.isGroupMessage) { - // For group messages, the main process (groupchat.js) is the single source of truth for history. - // The renderer avoids saving to prevent race conditions and overwriting the correct history. - return; - } - - const { agentId, topicId } = context; - - if (!agentId || !topicId) return; - - const historyToSave = history.filter(msg => !msg.isThinking && !msg.isPendingStream); - - try { - await electronAPI.saveChatHistory(agentId, topicId, historyToSave); - } catch (e) { - console.error(`[StreamManager] Failed to save history for context`, context, e); - } -} +function isMessageForCurrentView(context) { + if (!context) return false; + return refs.viewAuthority.isCurrent(context) === true; +} /** * 批量应用流式渲染所需的轻量级预处理。 * P0-1 后仅作为 parseTail 缺失时的兜底;正常路径由 messageRenderer 注入的 parseTail 统一处理。 */ -function applyStreamingPreprocessors(text) { - if (!text) return ''; - if (!contentPipeline) return text; - - return contentPipeline.process(text, { - mode: PIPELINE_MODES.STREAM_FAST - }).text; -} - +function applyStreamingPreprocessors(text) { + if (!text) return ''; + if (!contentRuntime) return text; + return contentRuntime.createRenderModel({ content: text, role: 'assistant' }, { mode: PIPELINE_MODES.STREAM_FAST }).text; +} + function parseStreamTail(text) { if (typeof refs.parseTail === 'function') { return refs.parseTail(text); @@ -436,19 +401,19 @@ function ensureStreamingRoots(contentDiv) { if (!stableRoot || !tailRoot) { contentDiv.innerHTML = ''; - stableRoot = document.createElement('div'); + stableRoot = ownerDocument().createElement('div'); stableRoot.className = 'vcp-stream-stable-root'; - stableBlocksRoot = document.createElement('div'); + stableBlocksRoot = ownerDocument().createElement('div'); stableBlocksRoot.className = 'vcp-stream-stable-blocks-root'; stableRoot.appendChild(stableBlocksRoot); - tailRoot = document.createElement('div'); + tailRoot = ownerDocument().createElement('div'); tailRoot.className = 'vcp-stream-tail-root'; contentDiv.appendChild(stableRoot); contentDiv.appendChild(tailRoot); } else if (!stableBlocksRoot) { // 兼容旧的 stableRoot 结构:后续追加式固化只写入 stableBlocksRoot。 // 如果 stableRoot 已有旧内容,先原样搬入 blocksRoot,避免切换实现时丢失已渲染 DOM。 - stableBlocksRoot = document.createElement('div'); + stableBlocksRoot = ownerDocument().createElement('div'); stableBlocksRoot.className = 'vcp-stream-stable-blocks-root'; while (stableRoot.firstChild) { stableBlocksRoot.appendChild(stableRoot.firstChild); @@ -508,7 +473,7 @@ function appendStableBlockFragment(stableBlocksRoot, segmentState, sourceText, h settings = null } = options; - const blockEl = document.createElement('div'); + const blockEl = ownerDocument().createElement('div'); const blockRecord = createStableBlockRecord( segmentState, segmentState.stableRenderedCutoff, @@ -588,7 +553,7 @@ function restoreStableBlocksForRecreatedDom(stableBlocksRoot, segmentState, opti stableBlocksRoot.replaceChildren(); for (const record of segmentState.stableBlocks) { - const blockEl = document.createElement('div'); + const blockEl = ownerDocument().createElement('div'); blockEl.className = 'vcp-stream-stable-block'; blockEl.dataset.vcpStreamStableBlock = 'true'; blockEl.dataset.vcpBlockKey = record.id; @@ -1162,7 +1127,7 @@ function findExplicitStablePrefix(text, startOffset = 0) { /** * 获取或缓存消息的 DOM 引用 */ -function getCachedMessageDom(messageId) { +function getCachedMessageDom(messageId) { let cached = messageDomCache.get(messageId); if (cached) { @@ -1175,8 +1140,7 @@ function getCachedMessageDom(messageId) { } // 重新查询并缓存 - const { chatMessagesDiv } = refs; - const messageItem = chatMessagesDiv.querySelector(`.message-item[data-message-id="${messageId}"]`); + const messageItem = refs.chatMessagesDiv?.querySelector?.(`.message-item[data-message-id="${messageId}"]`); if (!messageItem) return null; @@ -1186,9 +1150,9 @@ function getCachedMessageDom(messageId) { cached = { messageItem, contentDiv }; messageDomCache.set(messageId, cached); - return cached; -} - + return cached; +} + /** * Sets up onload and onerror handlers for an emoticon image to fix its URL on error * and prevent flickering by controlling its visibility. @@ -1242,7 +1206,8 @@ function processStreamTailImages(container) { function highlightCompletedStreamingCodeLine(lineElement) { if (!lineElement || lineElement.dataset.vcpStreamCodeHighlighted === 'true') return; - if (!window.hljs) return; + const syntaxHighlighter = refs.hljs || ownerWindow()?.hljs; + if (!syntaxHighlighter) return; const codeElement = lineElement.closest('code'); const languageClass = codeElement @@ -1256,14 +1221,14 @@ function highlightCompletedStreamingCodeLine(lineElement) { try { let highlightedHtml = ''; - if (lineText && language && window.hljs.getLanguage?.(language)) { - highlightedHtml = window.hljs.highlight(lineText, { + if (lineText && language && syntaxHighlighter.getLanguage?.(language)) { + highlightedHtml = syntaxHighlighter.highlight(lineText, { language, ignoreIllegals: true }).value; } else if (lineText) { // 没有语言标记时也只在该行完成时自动检测一次,而不是每帧重复检测。 - highlightedHtml = window.hljs.highlightAuto(lineText).value; + highlightedHtml = syntaxHighlighter.highlightAuto(lineText).value; } if (highlightedHtml) { @@ -1279,9 +1244,9 @@ function highlightCompletedStreamingCodeLine(lineElement) { function playStreamingCodeLineSweep(lineElement, delayMs = 0) { if (!lineElement || typeof lineElement.animate !== 'function') return; - if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return; + if (ownerWindow()?.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return; - const isLightTheme = document.body.classList.contains('light-theme'); + const isLightTheme = ownerDocument()?.body?.classList.contains('light-theme'); const baseColor = isLightTheme ? '#333333' : '#abb2bf'; const colorSweep = isLightTheme ? `linear-gradient(90deg, transparent 0%, transparent 24%, #0077b6 37%, #6f42c1 46%, #d63384 54%, #b26a00 63%, #238636 70%, transparent 82%, transparent 100%)` @@ -1376,7 +1341,7 @@ function renderStreamFrame(messageId) { // 如果没有缓存(可能是旧消息),回退到实时检查 if (isForCurrentView === undefined) { const context = messageContextMap.get(messageId); - isForCurrentView = isMessageForCurrentView(context); + isForCurrentView = isMessageForCurrentView(context); viewContextCache.set(messageId, isForCurrentView); } @@ -1474,7 +1439,7 @@ function renderStreamFrame(messageId) { if (lengthDiff > 20) { // 使用脉冲动画而不是滑入动画 fromEl.classList.add('vcp-stream-content-pulse'); - setTimeout(() => { + scheduleOwnedTimeout(() => { fromEl.classList.remove('vcp-stream-content-pulse'); }, 300); } @@ -1498,8 +1463,8 @@ function renderStreamFrame(messageId) { } // 🟢 保留输入焦点 - if (fromEl === document.activeElement) { - requestAnimationFrame(() => toEl.focus()); + if (fromEl === ownerDocument()?.activeElement) { + scheduleAnimationFrame(() => toEl.focus()); } // 🟢 简化图片逻辑:只保留状态,不再做 URL 对比 @@ -1552,7 +1517,7 @@ function renderStreamFrame(messageId) { elementContentLengthCache.set(node, node.textContent.length); // 动画结束后清理类名,但保留一小段时间确保渲染稳定 - setTimeout(() => { + scheduleOwnedTimeout(() => { if (node && node.classList) { node.classList.remove('vcp-stream-element-fade-in'); } @@ -1580,15 +1545,15 @@ function renderStreamFrame(messageId) { /** * 🟢 节流版本的滚动函数 */ -function throttledScrollToBottom(messageId) { +function throttledScrollToBottom(messageId) { if (scrollThrottleTimers.has(messageId)) { return; // 节流期间,跳过 } - refs.uiHelper.scrollToBottom(); + refs.uiHelper.scrollToBottom(); - const timerId = setTimeout(() => { - scrollThrottleTimers.delete(messageId); + const timerId = scheduleOwnedTimeout(() => { + scrollThrottleTimers.delete(messageId); }, SCROLL_THROTTLE_MS); scrollThrottleTimers.set(messageId, timerId); @@ -1627,9 +1592,9 @@ function processAndRenderSmoothChunk(messageId) { // Scroll if the message is in the current view. const context = messageContextMap.get(messageId); - if (isMessageForCurrentView(context)) { - throttledScrollToBottom(messageId); - } + if (isMessageForCurrentView(context)) { + throttledScrollToBottom(messageId); + } } function renderChunkDirectlyToDOM(messageId, textToAppend) { @@ -1641,15 +1606,35 @@ function renderChunkDirectlyToDOM(messageId, textToAppend) { } } -export async function startStreamingMessage(message, passedMessageItem = null) { - const messageId = message.id; +function conversationIdentityMatches(context, identity = {}) { + if (!context || !identity) return false; + const itemType = context.isGroupMessage ? 'group' : 'agent'; + const itemId = context.isGroupMessage ? context.groupId : context.agentId; + return itemType === identity.itemType + && itemId === identity.itemId + && context.topicId === identity.topicId; +} + +async function startStreamingMessage(message, passedMessageItem = null) { + if (disposed) return null; + const messageId = message.id; + const streamOperationId = message.streamOperationId || message.context?.streamOperationId || null; + const previousRuntimeKey = messageRuntimeKeys.get(String(messageId)); + const nextRuntimeKey = streamOperationId ? `${streamOperationId}::${messageId}` : String(messageId); + if (previousRuntimeKey && previousRuntimeKey !== nextRuntimeKey) { + // Keep the old key active while discardStreamingMessage clears every + // operation-owned map; only then publish the retry's new owner key. + discardStreamingMessage(messageId); + } + messageRuntimeKeys.set(String(messageId), nextRuntimeKey); // 🟢 修复:如果消息已在处理中,且 isThinking 状态没变,直接返回现有状态 const currentStatus = messageInitializationStatus.get(messageId); const cached = getCachedMessageDom(messageId); const isCurrentlyThinking = cached?.messageItem?.classList.contains('thinking'); - if ((currentStatus === 'pending' || currentStatus === 'ready') && (isCurrentlyThinking === !!message.isThinking)) { + if ((currentStatus === 'pending' || currentStatus === 'ready') && cached + && (isCurrentlyThinking === !!message.isThinking)) { console.debug(`[StreamManager] Message ${messageId} already initialized (${currentStatus}) with same thinking state, skipping re-init`); return cached?.messageItem || null; } @@ -1661,9 +1646,10 @@ export async function startStreamingMessage(message, passedMessageItem = null) { topicId: message.topicId || message.context?.topicId || refs.currentTopicIdRef.get(), isGroupMessage: message.isGroupMessage || message.context?.isGroupMessage || false, agentName: message.name || message.context?.agentName, - avatarUrl: message.avatarUrl || message.context?.avatarUrl, - avatarColor: message.avatarColor || message.context?.avatarColor, - }; + avatarUrl: message.avatarUrl || message.context?.avatarUrl, + avatarColor: message.avatarColor || message.context?.avatarColor, + streamOperationId, + }; // Validate context if (!context.topicId || (!context.agentId && !context.groupId)) { @@ -1671,42 +1657,42 @@ export async function startStreamingMessage(message, passedMessageItem = null) { return null; } - messageContextMap.set(messageId, context); + messageContextMap.set(messageId, context); + streamMessageModels.set(messageId, { + ...message, + id: messageId, + agentId: context.agentId, + groupId: context.groupId, + topicId: context.topicId, + isGroupMessage: context.isGroupMessage, + streamOperationId, + }); // 🟢 关键修复:如果消息已经初始化过,不要重新设为 pending,避免阻塞后续 chunk if (!currentStatus || currentStatus === 'finalized') { messageInitializationStatus.set(messageId, 'pending'); } - activeStreamingMessageId = messageId; + activeStreamingMessages.set(messageId, context); - const { chatMessagesDiv, electronAPI, currentChatHistoryRef, uiHelper } = refs; - const isForCurrentView = isMessageForCurrentView(context); - // 🟢 缓存视图检查结果 - viewContextCache.set(messageId, isForCurrentView); + const { chatMessagesDiv, electronAPI, uiHelper } = refs; + const isForCurrentView = isMessageForCurrentView(context); + const shouldProjectToDom = isForCurrentView; + // 🟢 缓存视图检查结果 + viewContextCache.set(messageId, shouldProjectToDom); - // Get the correct history for this message's context - let historyForThisMessage; - // For assistant chat, always use a temporary in-memory history - if (context.topicId === 'assistant_chat' || context.topicId?.startsWith('voicechat_')) { - historyForThisMessage = currentChatHistoryRef.get(); - } else if (isForCurrentView) { - // For current view, use in-memory history - historyForThisMessage = currentChatHistoryRef.get(); - } else { - // For background chats, load from disk - historyForThisMessage = await getHistoryForContext(context); - if (!historyForThisMessage) { - console.error(`[StreamManager] Could not load history for background message ${messageId}`, context); - discardStreamingMessage(messageId); - return null; - } - } + try { + await refs.transientStreamHistory.prepare(message, context, { visible: isForCurrentView }); + } catch (error) { + console.error(`[StreamManager] Could not prepare transient history for ${messageId}`, context, error); + discardStreamingMessage(messageId); + return null; + } // Only manipulate DOM for current view let messageItem = null; - if (isForCurrentView) { - messageItem = passedMessageItem || chatMessagesDiv.querySelector(`.message-item[data-message-id="${message.id}"]`); + if (shouldProjectToDom) { + messageItem = passedMessageItem || chatMessagesDiv.querySelector(`.message-item[data-message-id="${message.id}"]`); if (!messageItem) { const placeholderMessage = { ...message, @@ -1715,7 +1701,9 @@ export async function startStreamingMessage(message, passedMessageItem = null) { timestamp: message.timestamp || Date.now(), isGroupMessage: message.isGroupMessage || false }; - messageItem = refs.renderMessage(placeholderMessage, false); + messageItem = refs.chatDomRenderer + ? await refs.chatDomRenderer.renderMessage(placeholderMessage, false) + : refs.renderMessage(placeholderMessage, false); if (!messageItem) { console.error(`[StreamManager] Failed to render message item for ${message.id}`); discardStreamingMessage(messageId); @@ -1723,9 +1711,11 @@ export async function startStreamingMessage(message, passedMessageItem = null) { } } // Add streaming class and remove thinking class when we have a valid messageItem - if (messageItem && messageItem.classList) { - messageItem.classList.add('streaming'); - messageItem.classList.remove('thinking'); + if (messageItem && messageItem.classList) { + messageItem.classList.add('streaming'); + messageItem.classList.remove('thinking'); + const contentDiv = messageItem.querySelector('.md-content'); + if (contentDiv) messageDomCache.set(messageId, { messageItem, contentDiv }); } } @@ -1748,40 +1738,7 @@ export async function startStreamingMessage(message, passedMessageItem = null) { accumulatedStreamText.set(messageId, newText); } - // Prepare placeholder for history - const placeholderForHistory = { - ...message, - content: shouldSkipGroupThinkingSeed ? '' : (message.content || ''), - isThinking: false, - isPendingStream: true, - timestamp: message.timestamp || Date.now(), - isGroupMessage: context.isGroupMessage, - name: context.agentName, - agentId: context.agentId - }; - pendingHistoryEntries.set(messageId, { ...placeholderForHistory }); - - // Update the appropriate history - const historyIndex = historyForThisMessage.findIndex(m => m.id === message.id); - if (historyIndex === -1) { - historyForThisMessage.push(placeholderForHistory); - } else { - historyForThisMessage[historyIndex] = { ...historyForThisMessage[historyIndex], ...placeholderForHistory }; - } - - // Save the history - if (isForCurrentView) { - // Update in-memory reference for current view - currentChatHistoryRef.set([...historyForThisMessage]); - window.updateSendButtonState?.(); - } - - // 🟢 使用防抖保存 - if (context.topicId !== 'assistant_chat' && !context.topicId.startsWith('voicechat_')) { - debouncedSaveHistory(context, historyForThisMessage); - } - - // Initialization is complete, message is ready to process chunks. + // Initialization is complete, message is ready to process chunks. // 如果 end/error 事件在异步初始化期间已经到达,不能把状态从 finalized 回退到 ready。 if (messageInitializationStatus.get(messageId) !== 'finalized') { messageInitializationStatus.set(messageId, 'ready'); @@ -1792,31 +1749,21 @@ export async function startStreamingMessage(message, passedMessageItem = null) { if (bufferedChunks && bufferedChunks.length > 0 && messageInitializationStatus.get(messageId) === 'ready') { console.debug(`[StreamManager] Processing ${bufferedChunks.length} pre-buffered chunks for message ${messageId}`); for (const chunkData of bufferedChunks) { - appendStreamChunk(messageId, chunkData.chunk, chunkData.context); + appendStreamChunk(messageId, chunkData.chunk, chunkData.context, chunkData.streamOperationId); } preBufferedChunks.delete(messageId); } - const deferredFinalization = pendingFinalizationEvents.get(messageId); - if (deferredFinalization) { - pendingFinalizationEvents.delete(messageId); - console.warn(`[StreamManager] Replaying deferred finalization for message ${messageId}.`); - setTimeout(() => { - finalizeStreamedMessage( - messageId, - deferredFinalization.finishReason, - deferredFinalization.context, - deferredFinalization.finalPayload - ); - }, 0); - } + const initializationWaiters = messageInitializationWaiters.get(messageId) || []; + messageInitializationWaiters.delete(messageId); + initializationWaiters.forEach(resolve => resolve(true)); - if (isForCurrentView) { - // 如果从思考转为非思考,立即触发一次渲染以清理占位符 + if (shouldProjectToDom) { + // 如果从思考转为非思考,立即触发一次渲染以清理占位符 if (!message.isThinking && isCurrentlyThinking) { renderStreamFrame(messageId); } - uiHelper.scrollToBottom(); + uiHelper.scrollToBottom(); } return messageItem; @@ -1848,7 +1795,7 @@ function startGlobalRenderLoop() { } const elapsed = currentTime - lastFrameTime; if (elapsed < FRAME_INTERVAL) { - requestAnimationFrame(renderLoop); + scheduleAnimationFrame(renderLoop); return; } @@ -1874,10 +1821,10 @@ function startGlobalRenderLoop() { } } - requestAnimationFrame(renderLoop); + scheduleAnimationFrame(renderLoop); } - requestAnimationFrame(renderLoop); + scheduleAnimationFrame(renderLoop); } /** @@ -1925,198 +1872,17 @@ function intelligentChunkSplit(text) { * 2. preprocessFullContent 中已经通过 toolResultMap 保护了工具结果块 * 3. 在逐字符级别做工具结果块检测会与推送标签检测产生字符竞争bug */ -function processDesktopPushToken(messageId, textToAppend) { - let state = desktopPushStates.get(messageId); - if (!state) { - state = { active: false, widgetId: null, buffer: '', tagBuffer: '', created: false, validated: false, pushTimer: null, lastPushedLength: 0, lastTokenTime: null, backtickContext: false }; - desktopPushStates.set(messageId, state); - } - - const electronAPI = refs.electronAPI; - const canPush = desktopWindowAvailable && electronAPI?.desktopPush; - - let remainingText = textToAppend; - let outputText = ''; - - for (let i = 0; i < remainingText.length; i++) { - const char = remainingText[i]; - - if (!state.active) { - state.tagBuffer += char; - - if (DESKTOP_PUSH_START_TAG.startsWith(state.tagBuffer)) { - if (state.tagBuffer === DESKTOP_PUSH_START_TAG) { - // 🟢 加固:检查开始标签前是否有反引号包裹 - // 检查 outputText 末尾是否刚输出了一个反引号 - const precedingChar = outputText.length > 0 ? outputText[outputText.length - 1] : ''; - if (precedingChar === '`') { - // 被反引号包裹,不视为推送标签,直接输出原文 - state.backtickContext = true; - outputText += state.tagBuffer; - state.tagBuffer = ''; - continue; - } - - // 匹配到开始标签,进入active状态但延迟创建挂件 - state.active = true; - state.backtickContext = false; - state.widgetId = 'dw-' + Date.now().toString(36) + Math.random().toString(36).substring(2, 5); - state.buffer = ''; - state.created = false; - state.validated = false; // 二级验证:等待内容前缀确认 - state.tagBuffer = ''; - state.lastPushedLength = 0; - } - } else { - outputText += state.tagBuffer; - state.tagBuffer = ''; - } - } else { - // 在推送块内 - state.tagBuffer += char; - - if (DESKTOP_PUSH_END_TAG.startsWith(state.tagBuffer)) { - if (state.tagBuffer === DESKTOP_PUSH_END_TAG) { - // 结束标签 - if (state.pushTimer) { clearInterval(state.pushTimer); state.pushTimer = null; } - - if (canPush && state.created) { - if (state.isReplaceMode) { - // 替换模式:解析 target/replace 的「始ESCAPE」「末ESCAPE」或旧版「始」「末」 - const targetMatch = state.buffer.match(/target:(?:「始ESCAPE」([\s\S]*?)「末ESCAPE」|「始」([\s\S]*?)「末」)/); - const replaceMatch = state.buffer.match(/replace:(?:「始ESCAPE」([\s\S]*?)「末ESCAPE」|「始」([\s\S]*?)「末」)/); - - if (targetMatch && replaceMatch) { - const targetSelector = (targetMatch[1] || targetMatch[2] || '').trim(); - const replaceContent = (replaceMatch[1] || replaceMatch[2] || '').trim(); - electronAPI.desktopPush({ - action: 'replace', - targetSelector: targetSelector, - content: replaceContent - }); - console.log(`[DesktopPush] Replace: "${targetSelector}" → ${replaceContent.substring(0, 50)}...`); - } else { - console.warn(`[DesktopPush] Replace mode but couldn't parse target/replace fields from buffer:`, state.buffer.substring(0, 100)); - } - } else { - // 创建模式:最终推送 + finalize - electronAPI.desktopPush({ action: 'append', widgetId: state.widgetId, content: state.buffer }); - electronAPI.desktopPush({ action: 'finalize', widgetId: state.widgetId }); - console.log(`[DesktopPush] Widget finalized: ${state.widgetId}`); - } - } - - state.active = false; state.tagBuffer = ''; state.buffer = ''; - state.widgetId = null; state.created = false; state.validated = false; - state.isReplaceMode = false; state.lastPushedLength = 0; - } - } else { - // 不是结束标签,内容追加到buffer - state.buffer += state.tagBuffer; - state.tagBuffer = ''; - - // 🟢 性能优化:仅更新时间戳,超时检查由 pushTimer interval 负责 - // 这样每个 token 只需一次赋值操作,避免频繁 clearTimeout/setTimeout - state.lastTokenTime = Date.now(); - - // 二级验证:buffer积累到一定量后检查前缀是否合法 - // 只在前30个有效字符内做验证,避免延迟过大 - if (!state.validated && state.buffer.trim().length >= 5) { - const trimmedBuffer = state.buffer.trim().toLowerCase(); - const isValid = DESKTOP_PUSH_VALID_PREFIXES.some(prefix => trimmedBuffer.startsWith(prefix)); - - if (isValid) { - state.validated = true; - - // 判断是否为替换模式(target:「始」...「末」开头) - const isReplaceMode = trimmedBuffer.startsWith('target:'); - state.isReplaceMode = isReplaceMode; - - if (isReplaceMode) { - console.log(`[DesktopPush] Replace mode detected, waiting for target and replace fields...`); - state.created = true; // 标记为已处理,但不创建新挂件 - // 替换模式不需要定时推送,等到结束标签时一次性解析并替换 - } else { - console.log(`[DesktopPush] Content validated with prefix: ${trimmedBuffer.substring(0, 15)}...`); - - // 创建模式:验证通过后才创建挂件 - if (canPush) { - electronAPI.desktopPush({ - action: 'create', widgetId: state.widgetId, - options: { x: 200, y: 150, width: 400, height: 300 } - }); - state.created = true; - - // 启动定时推送 + 内置空闲超时检测 - state.lastTokenTime = Date.now(); - state.pushTimer = setInterval(() => { - // 推送新内容 - if (state.buffer.length > state.lastPushedLength) { - electronAPI.desktopPush({ - action: 'append', widgetId: state.widgetId, content: state.buffer - }); - state.lastPushedLength = state.buffer.length; - } - - // 🟢 空闲超时检测:如果距离上次token超过150秒,自动finalize - // 不需要单独的setTimeout,复用已有的interval,零额外开销 - if (state.lastTokenTime && (Date.now() - state.lastTokenTime > DESKTOP_PUSH_TIMEOUT_MS)) { - console.warn(`[DesktopPush] Widget ${state.widgetId} idle timeout (no new tokens for ${DESKTOP_PUSH_TIMEOUT_MS / 1000}s), auto-finalizing`); - clearInterval(state.pushTimer); state.pushTimer = null; - if (state.created && !state.isReplaceMode && electronAPI?.desktopPush) { - electronAPI.desktopPush({ action: 'append', widgetId: state.widgetId, content: state.buffer }); - electronAPI.desktopPush({ action: 'finalize', widgetId: state.widgetId }); - } - state.active = false; state.tagBuffer = ''; state.buffer = ''; - state.widgetId = null; state.created = false; state.validated = false; - state.isReplaceMode = false; state.lastPushedLength = 0; state.lastTokenTime = null; - } - }, DESKTOP_PUSH_THROTTLE_MS); - } - } - - // 🟢 替换模式也需要空闲超时保护 - // 替换模式没有 pushTimer,需要单独的超时机制 - if (state.isReplaceMode && canPush) { - state.lastTokenTime = Date.now(); - // 替换模式用一个轻量级的检查 interval - state.pushTimer = setInterval(() => { - if (state.lastTokenTime && (Date.now() - state.lastTokenTime > DESKTOP_PUSH_TIMEOUT_MS)) { - console.warn(`[DesktopPush] Replace mode idle timeout, discarding`); - clearInterval(state.pushTimer); state.pushTimer = null; - state.active = false; state.tagBuffer = ''; state.buffer = ''; - state.widgetId = null; state.created = false; state.validated = false; - state.isReplaceMode = false; state.lastPushedLength = 0; state.lastTokenTime = null; - } - }, 5000); // 替换模式检查频率低一些:5秒一次 - } - } else if (state.buffer.trim().length >= 30) { - // 验证失败:30字符内未匹配到合法前缀,丢弃该推送块 - console.warn(`[DesktopPush] Invalid content prefix, discarding push block: "${trimmedBuffer.substring(0, 30)}..."`); - state.active = false; state.tagBuffer = ''; state.buffer = ''; - state.widgetId = null; state.created = false; state.validated = false; state.lastPushedLength = 0; - } - // 5-30字符之间继续等待更多内容 - } - } - } - } - - return outputText; -} -/** - * 清理消息的桌面推送状态 - */ -function cleanupDesktopPushState(messageId) { - const state = desktopPushStates.get(messageId); - if (state?.pushTimer) { - clearInterval(state.pushTimer); - state.pushTimer = null; - } - desktopPushStates.delete(messageId); -} +function processDesktopPushToken(messageId, textToAppend) { + return desktopPushConsumer?.processToken(messageId, textToAppend) ?? textToAppend; +} + +/** Releases the Desktop canvas state owned by one stream message. */ +function cleanupDesktopPushState(messageId) { + desktopPushConsumer?.cleanupMessage(messageId); +} -export function appendStreamChunk(messageId, chunkData, context) { +function appendStreamChunk(messageId, chunkData, context, streamOperationId = null) { + if (disposed) return false; const initStatus = messageInitializationStatus.get(messageId); if (!initStatus || initStatus === 'pending') { @@ -2126,7 +1892,7 @@ export function appendStreamChunk(messageId, chunkData, context) { console.debug(`[StreamManager] Started pre-buffering for message ${messageId}`); } const buffer = preBufferedChunks.get(messageId); - buffer.push({ chunk: chunkData, context }); + buffer.push({ chunk: chunkData, context, streamOperationId }); // 防止缓冲区无限增长 - 如果超过1000个chunks,可能有问题 if (buffer.length > 1000) { @@ -2142,26 +1908,18 @@ export function appendStreamChunk(messageId, chunkData, context) { return; } - // Extract text from chunk + const activeContext = messageContextMap.get(messageId); + if (streamOperationId && activeContext?.streamOperationId && activeContext.streamOperationId !== streamOperationId) return false; + // Extract text from chunk // 如果检测到 JSON 解析错误,直接过滤掉,不显示给用户 if (chunkData?.error === 'json_parse_error') { console.warn(`[StreamManager] 过滤掉 JSON 解析错误的 chunk for messageId: ${messageId}`, chunkData.raw); return; } - let textToAppend = ""; - if (chunkData?.choices?.[0]?.delta?.content) { - textToAppend = chunkData.choices[0].delta.content; - } else if (chunkData?.delta?.content) { - textToAppend = chunkData.delta.content; - } else if (typeof chunkData?.content === 'string') { - textToAppend = chunkData.content; - } else if (typeof chunkData === 'string') { - textToAppend = chunkData; - } else if (chunkData?.raw && !chunkData?.error) { - // 只有在没有错误标记时才显示 raw 数据 - textToAppend = chunkData.raw; - } + const textToAppend = contentRuntime + ? contentRuntime.extractChunkText(chunkData) + : ''; if (!textToAppend) return; @@ -2209,19 +1967,35 @@ export function appendStreamChunk(messageId, chunkData, context) { } } -export async function finalizeStreamedMessage(messageId, finishReason, context, finalPayload = null) { - const initStatusAtFinalize = messageInitializationStatus.get(messageId); - if (!initStatusAtFinalize || initStatusAtFinalize === 'pending') { - console.warn(`[StreamManager] Finalization arrived before message initialization completed for ${messageId}. Deferring. status=${initStatusAtFinalize || 'missing'}`); - pendingFinalizationEvents.set(messageId, { finishReason, context, finalPayload }); - return; - } +/** + * Applies the terminal message model and DOM projection without writing durable history. + * The StreamCoordinator owns the production commit point. + */ +async function projectStreamTerminal(messageId, finishReason, context, finalPayload = null) { + if (disposed) return null; + const expectedOperationId = finalPayload?.streamOperationId || context?.streamOperationId || null; + const activeContext = messageContextMap.get(messageId); + if (expectedOperationId && activeContext?.streamOperationId && activeContext.streamOperationId !== expectedOperationId) return null; + let initStatusAtFinalize = messageInitializationStatus.get(messageId); + if (initStatusAtFinalize === 'pending') { + console.warn(`[StreamManager] Finalization is waiting for message initialization: ${messageId}`); + const initialized = await new Promise(resolve => { + const waiters = messageInitializationWaiters.get(messageId) || []; + waiters.push(resolve); + messageInitializationWaiters.set(messageId, waiters); + }); + if (!initialized) return null; + initStatusAtFinalize = messageInitializationStatus.get(messageId); + } + if (!initStatusAtFinalize) { + console.warn(`[StreamManager] Finalization ignored because message initialization is absent: ${messageId}`); + discardStreamingMessage(messageId); + return null; + } // With the global render loop, we no longer need to manually drain the queue here or clear timers. // The loop will continue to process chunks until the queue is empty and the message is finalized, then clean itself up. - if (activeStreamingMessageId === messageId) { - activeStreamingMessageId = null; - } + activeStreamingMessages.delete(messageId); // 🟢 清理节流定时器 const scrollTimer = scrollThrottleTimers.get(messageId); @@ -2240,39 +2014,11 @@ export async function finalizeStreamedMessage(messageId, finishReason, context, return; } - const { chatMessagesDiv, uiHelper } = refs; - const isForCurrentView = isMessageForCurrentView(storedContext); - - // Get the correct history - let historyForThisMessage; - const currentViewHistory = refs.currentChatHistoryRef.get(); - const currentViewOwnsPendingMessage = isForCurrentView - && Array.isArray(currentViewHistory) - && currentViewHistory.some(message => message?.id === messageId); - // The thinking placeholder is committed to renderer memory before the - // request starts, but intentionally is not written to disk. When a fast - // stream finishes, disk can therefore lag behind and not contain the - // message yet. Prefer the owned in-memory transaction for the current - // view; background topics still read their own persisted history. - if ( - currentViewOwnsPendingMessage - || storedContext.topicId === 'assistant_chat' - || storedContext.topicId?.startsWith('voicechat_') - ) { - historyForThisMessage = currentViewHistory; - } else { - // For all other chats, always fetch the latest history from the source of truth - // to avoid race conditions with the UI state (currentChatHistoryRef). - historyForThisMessage = await getHistoryForContext(storedContext); - if (!historyForThisMessage) { - console.error(`[StreamManager] Could not load history for finalization`, storedContext); - discardStreamingMessage(messageId); - return; - } - } + const { chatMessagesDiv, uiHelper } = refs; + const isForCurrentView = isMessageForCurrentView(storedContext); + const shouldProjectToDom = isForCurrentView; - // Find and update the message - const accumulatedText = accumulatedStreamText.get(messageId) || ""; + const accumulatedText = accumulatedStreamText.get(messageId) || ""; const payloadFullResponse = typeof finalPayload?.fullResponse === 'string' ? finalPayload.fullResponse : ""; const payloadError = typeof finalPayload?.error === 'string' ? finalPayload.error.trim() : ""; const streamedTextIsUsable = accumulatedText.trim() !== "" && !isThinkingPlaceholderText(accumulatedText); @@ -2293,26 +2039,21 @@ export async function finalizeStreamedMessage(messageId, finishReason, context, finalFullText = ""; } } - let messageIndex = historyForThisMessage.findIndex(msg => msg.id === messageId); - - // A background stream can finish before its debounced history save. The - // pending entry lives in renderer memory rather than on disk; reconstruct - // it at the position owned by its user message before applying the final - // response. This also preserves conversational order when two streams - // finish in reverse order. - if (messageIndex === -1) { - const pendingEntry = pendingHistoryEntries.get(messageId); - if (pendingEntry) { - const replyIndex = pendingEntry.replyToMessageId - ? historyForThisMessage.findIndex(message => message.id === pendingEntry.replyToMessageId) - : -1; - const insertionIndex = replyIndex >= 0 ? replyIndex + 1 : historyForThisMessage.length; - historyForThisMessage.splice(insertionIndex, 0, { ...pendingEntry }); - messageIndex = insertionIndex; - } - } - - if (messageIndex === -1) { + let transientModel; + try { + transientModel = await refs.transientStreamHistory.finalize({ + messageId, + context: storedContext, + content: finalFullText, + finishReason, + visible: isForCurrentView, + }); + } catch (error) { + console.error(`[StreamManager] Could not finalize transient history for ${messageId}`, storedContext, error); + discardStreamingMessage(messageId); + return null; + } + if (!transientModel) { // If it's an assistant chat and the message is not found, // it's likely the window was reset. Ignore gracefully. if (storedContext && storedContext.topicId === 'assistant_chat') { @@ -2325,21 +2066,12 @@ export async function finalizeStreamedMessage(messageId, finishReason, context, return; } - const message = historyForThisMessage[messageIndex]; - message.content = finalFullText; - message.finishReason = finishReason; - message.isThinking = false; - delete message.isPendingStream; - if (message.isGroupMessage && storedContext) { - message.name = storedContext.agentName || message.name; - message.agentId = storedContext.agentId || message.agentId; - } + const { message, history: historyForThisMessage } = transientModel; // Update UI if it's the current view - if (isForCurrentView) { - refs.currentChatHistoryRef.set([...historyForThisMessage]); - - const messageItem = chatMessagesDiv.querySelector(`.message-item[data-message-id="${messageId}"]`); + if (shouldProjectToDom) { + const messageItem = messageDomCache.get(messageId)?.messageItem + || chatMessagesDiv.querySelector(`.message-item[data-message-id="${messageId}"]`); if (messageItem) { messageItem.classList.remove('streaming', 'thinking'); @@ -2377,7 +2109,7 @@ export async function finalizeStreamedMessage(messageId, finishReason, context, } // Step 2: Defer TreeWalker-based highlighters to ensure DOM is stable - setTimeout(() => { + scheduleOwnedTimeout(() => { if (contentDiv && contentDiv.isConnected) { refs.runTextHighlights(contentDiv); } @@ -2392,69 +2124,49 @@ export async function finalizeStreamedMessage(messageId, finishReason, context, const nameTimeBlock = messageItem.querySelector('.name-time-block'); if (nameTimeBlock && !nameTimeBlock.querySelector('.message-timestamp')) { - const timestampDiv = document.createElement('div'); + const timestampDiv = ownerDocument().createElement('div'); timestampDiv.classList.add('message-timestamp'); timestampDiv.textContent = formatMessageTimestamp(message.timestamp || Date.now()); nameTimeBlock.appendChild(timestampDiv); } - uiHelper.scrollToBottom(); + uiHelper.scrollToBottom(); } } - // 🟢 使用防抖保存 - if (storedContext.topicId !== 'assistant_chat') { - debouncedSaveHistory(storedContext, historyForThisMessage); - } - - // Cleanup + // Cleanup streamingChunkQueues.delete(messageId); pendingDirectRenderMessages.delete(messageId); - accumulatedStreamText.delete(messageId); - streamSegmentStates.delete(messageId); - pendingHistoryEntries.delete(messageId); - cleanupDesktopPushState(messageId); - - // Finalization can start in a background topic and finish after the user - // has navigated again. Refreshing only inside isForCurrentView leaves the - // shared send button stuck in interrupt mode even though all stream state - // is already gone. The updater derives state from the *current* chat, so - // it is safe and necessary to run after cleanup for every finalization. - window.updateSendButtonState?.(); - - // Delayed cleanup - const existingCleanupTimer = delayedCleanupTimers.get(messageId); - if (existingCleanupTimer) { - clearTimeout(existingCleanupTimer); - } - const cleanupTimerId = setTimeout(() => { - messageDomCache.delete(messageId); - messageInitializationStatus.delete(messageId); - preBufferedChunks.delete(messageId); - messageContextMap.delete(messageId); - pendingHistoryEntries.delete(messageId); - viewContextCache.delete(messageId); - delayedCleanupTimers.delete(messageId); - }, 5000); - delayedCleanupTimers.set(messageId, cleanupTimerId); + accumulatedStreamText.delete(messageId); + streamSegmentStates.delete(messageId); + refs.transientStreamHistory.discard?.(messageId); + cleanupDesktopPushState(messageId); + + // Terminal means ownership has ended; no delayed cache lease survives `done`. + messageDomCache.delete(messageId); + messageInitializationStatus.delete(messageId); + preBufferedChunks.delete(messageId); + messageContextMap.delete(messageId); + viewContextCache.delete(messageId); + messageRuntimeKeys.delete(String(messageId)); + streamMessageModels.delete(messageId); // 调用方(例如 Flowlock)需要基于真正落盘的完整文本解析最终控制协议。 - return { - messageId, - context: storedContext, - content: finalFullText, - finishReason - }; -} - -export function discardStreamingMessage(messageId) { + return { + messageId, + context: storedContext, + content: finalFullText, + finishReason, + history: historyForThisMessage, + }; +} + +function discardStreamingMessage(messageId) { + if (disposed) return false; const scrollTimer = scrollThrottleTimers.get(messageId); if (scrollTimer) clearTimeout(scrollTimer); scrollThrottleTimers.delete(messageId); - const cleanupTimer = delayedCleanupTimers.get(messageId); - if (cleanupTimer) clearTimeout(cleanupTimer); - delayedCleanupTimers.delete(messageId); streamingChunkQueues.delete(messageId); streamingTimers.delete(messageId); pendingDirectRenderMessages.delete(messageId); @@ -2463,40 +2175,37 @@ export function discardStreamingMessage(messageId) { messageDomCache.delete(messageId); preBufferedChunks.delete(messageId); messageInitializationStatus.delete(messageId); - pendingFinalizationEvents.delete(messageId); - pendingHistoryEntries.delete(messageId); - messageContextMap.delete(messageId); + const initializationWaiters = messageInitializationWaiters.get(messageId) || []; + messageInitializationWaiters.delete(messageId); + initializationWaiters.forEach(resolve => resolve(false)); + refs.transientStreamHistory.discard?.(messageId); + messageContextMap.delete(messageId); viewContextCache.delete(messageId); cleanupDesktopPushState(messageId); - if (activeStreamingMessageId === messageId) activeStreamingMessageId = null; - window.updateSendButtonState?.(); -} + activeStreamingMessages.delete(messageId); + messageRuntimeKeys.delete(String(messageId)); +} -export function cleanupTransientState() { - // 清理所有流式消息相关状态 - for (const timerId of scrollThrottleTimers.values()) { - clearTimeout(timerId); - } - scrollThrottleTimers.clear(); - - for (const state of desktopPushStates.values()) { - if (state?.pushTimer) { - clearInterval(state.pushTimer); - } - } - desktopPushStates.clear(); +async function dispose() { + if (disposed) return; + disposed = true; + if (transientCleanupRegistered) { + transientCleanupWindow?.removeEventListener('beforeunload', dispose); + transientCleanupRegistered = false; + transientCleanupWindow = null; + } + cancelScheduledAnimationFrames(); + // 清理所有流式消息相关状态 + for (const timerId of scrollThrottleTimers.values()) { + clearOwnedTimeout(timerId); + } + scrollThrottleTimers.clear(); + for (const timeout of ownedTimeouts) clearOwnedTimeout(timeout); + cancelScheduledAnimationFrames(); - for (const timerId of delayedCleanupTimers.values()) { - clearTimeout(timerId); - } - delayedCleanupTimers.clear(); + desktopPushConsumer?.dispose(); + desktopPushConsumer = null; - for (const timerId of historySaveQueue.values()) { - if (timerId?.timerId) { - clearTimeout(timerId.timerId); - } - } - historySaveQueue.clear(); streamingChunkQueues.clear(); streamingTimers.clear(); @@ -2506,53 +2215,107 @@ export function cleanupTransientState() { messageDomCache.clear(); preBufferedChunks.clear(); messageInitializationStatus.clear(); - pendingFinalizationEvents.clear(); - pendingHistoryEntries.clear(); - messageContextMap.clear(); - viewContextCache.clear(); - - activeStreamingMessageId = null; + for (const waiters of messageInitializationWaiters.values()) waiters.forEach(resolve => resolve(false)); + messageInitializationWaiters.clear(); + refs.transientStreamHistory.dispose?.(); + messageContextMap.clear(); + viewContextCache.clear(); + streamMessageModels.clear(); + + activeStreamingMessages.clear(); + messageRuntimeKeys.clear(); currentViewSignature = null; - globalRenderLoopRunning = false; + globalRenderLoopRunning = false; + await Promise.allSettled([...pendingAsyncOperations]); console.debug('[StreamManager] Transient state cleared'); } -export function getStreamDiagnostics() { +function getActiveStreamingMessageId() { + const active = [...activeStreamingMessages.entries()]; + for (let index = active.length - 1; index >= 0; index -= 1) { + const [messageId, context] = active[index]; + if (isMessageForCurrentView(context)) return messageId; + } + return null; +} + +function getActiveStreamingContext() { + const messageId = getActiveStreamingMessageId(); + return messageId ? activeStreamingMessages.get(messageId) || null : null; +} + +function snapshotConversation(identity) { + if (disposed) return Object.freeze([]); + const snapshots = []; + for (const [messageId, context] of activeStreamingMessages.entries()) { + if (!conversationIdentityMatches(context, identity)) continue; + snapshots.push(Object.freeze({ + conversation: Object.freeze({ + itemType: context.isGroupMessage ? 'group' : 'agent', + itemId: context.isGroupMessage ? context.groupId : context.agentId, + topicId: context.topicId, + }), + message: Object.freeze({ ...(streamMessageModels.get(messageId) || { id: messageId }) }), + messageId, + streamOperationId: context.streamOperationId || null, + context: Object.freeze({ ...context }), + phase: messageInitializationStatus.get(messageId) || 'active', + accumulatedText: accumulatedStreamText.get(messageId) || '', + })); + } + return Object.freeze(snapshots); +} + +async function reconcileConversation(identity) { + if (disposed) return Object.freeze([]); + const snapshots = snapshotConversation(identity); + for (const snapshot of snapshots) { + const context = messageContextMap.get(snapshot.messageId) || snapshot.context; + viewContextCache.set(snapshot.messageId, isMessageForCurrentView(context)); + await startStreamingMessage({ ...snapshot.message, ...context, content: snapshot.message.content || '' }); + if (isMessageForCurrentView(context)) { + renderStreamFrame(snapshot.messageId); + } + } + return snapshots; +} + +function getStreamDiagnostics() { + const activeMessageId = getActiveStreamingMessageId(); return Object.freeze({ - activeMessageId: activeStreamingMessageId, + activeMessageId, + activeMessageIds: Object.freeze(activeStreamingMessages.displayKeys()), initialization: messageInitializationStatus.size, activeInitializations: [...messageInitializationStatus.values()] .filter(status => status === 'pending' || status === 'ready').length, contexts: messageContextMap.size, - pendingHistory: pendingHistoryEntries.size, + pendingHistory: refs.transientStreamHistory?.pendingCount || 0, prebuffered: preBufferedChunks.size, - pendingFinalizations: pendingFinalizationEvents.size, + pendingFinalizations: messageInitializationWaiters.size, chunkQueues: streamingChunkQueues.size, renderTimers: streamingTimers.size, - delayedCleanupTimers: delayedCleanupTimers.size, - historySaveQueue: historySaveQueue.size, - desktopPushStates: desktopPushStates.size, + delayedCleanupTimers: ownedTimeouts.size + scheduledAnimationFrames.size, + desktopPushStates: desktopPushConsumer?.getStateCount() || 0, }); } -// Expose to global scope for classic scripts -window.streamManager = { - initStreamManager, - startStreamingMessage, - appendStreamChunk, - finalizeStreamedMessage, - discardStreamingMessage, - cleanupTransientState, +return Object.freeze({ + attachStreamProjection, + dispose, + startStreamingMessage: (...args) => trackAsyncOperation(startStreamingMessage(...args)), + appendStreamChunk, + projectStreamTerminal: (...args) => trackAsyncOperation(projectStreamTerminal(...args)), + discardStreamingMessage, getDiagnostics: getStreamDiagnostics, isMessageActive, - getActiveStreamingMessageId: () => activeStreamingMessageId, - getActiveStreamingContext: () => { - if (!activeStreamingMessageId) return null; - return messageContextMap.get(activeStreamingMessageId) || null; - }, + getActiveStreamingMessageId, + getActiveStreamingContext, + snapshotConversation, + reconcileConversation: (...args) => trackAsyncOperation(reconcileConversation(...args)), isMessageInitialized: (messageId) => { // Check if message is being tracked by streamManager return messageInitializationStatus.has(messageId); } -}; +}); +} diff --git a/modules/renderer/streamProjectionRuntime.js b/modules/renderer/streamProjectionRuntime.js new file mode 100644 index 000000000..50d769985 --- /dev/null +++ b/modules/renderer/streamProjectionRuntime.js @@ -0,0 +1,52 @@ +/** + * Surface-owned mutable projection runtime. This module contains no DOM, + * Electron or history policy; the projection factory owns one instance. + */ +export function createStreamProjectionRuntime() { + const messageRuntimeKeys = new Map(); + const runtimeStateKey = messageId => messageRuntimeKeys.get(String(messageId)) || String(messageId); + const createMap = () => { + const store = new Map(); + return { + get: key => store.get(runtimeStateKey(key)), + has: key => store.has(runtimeStateKey(key)), + set(key, value) { store.set(runtimeStateKey(key), value); return this; }, + delete: key => store.delete(runtimeStateKey(key)), + clear: () => store.clear(), + get size() { return store.size; }, + keys: () => [...store.keys()].map(key => String(key).split('::').pop())[Symbol.iterator](), + values: () => store.values(), + entries: () => [...store.entries()].map(([key, value]) => [String(key).split('::').pop(), value])[Symbol.iterator](), + forEach: callback => store.forEach((value, key) => callback(value, String(key).split('::').pop(), store)), + [Symbol.iterator]() { return this.entries(); }, + displayKeys: () => [...store.keys()].map(key => String(key).split('::').pop()), + }; + }; + const createSet = () => { + const store = new Set(); + return { + add(key) { store.add(runtimeStateKey(key)); return this; }, + has: key => store.has(runtimeStateKey(key)), + delete: key => store.delete(runtimeStateKey(key)), + clear: () => store.clear(), + get size() { return store.size; }, + }; + }; + return { + messageRuntimeKeys, + streamingChunkQueues: createMap(), + streamingTimers: createMap(), + accumulatedStreamText: createMap(), + streamSegmentStates: createMap(), + activeStreamingMessages: createMap(), + messageDomCache: createMap(), + scrollThrottleTimers: createMap(), + viewContextCache: createMap(), + pendingDirectRenderMessages: createSet(), + preBufferedChunks: createMap(), + messageInitializationStatus: createMap(), + messageInitializationWaiters: createMap(), + messageContextMap: createMap(), + elementContentLengthCache: new WeakMap(), + }; +} diff --git a/modules/renderer/surfaceTaskOwner.js b/modules/renderer/surfaceTaskOwner.js new file mode 100644 index 000000000..da2fff5df --- /dev/null +++ b/modules/renderer/surfaceTaskOwner.js @@ -0,0 +1,82 @@ +function createState() { + return { generation: 0, scheduled: new Set(), running: new Set(), disposed: false }; +} + +/** Owns scheduler handles and already-started work for one DOM Surface root. */ +export function createSurfaceTaskOwner({ environmentForRoot = root => root?.ownerDocument?.defaultView || globalThis } = {}) { + const states = new WeakMap(); + const stateFor = root => { + if (!root || (typeof root !== 'object' && typeof root !== 'function')) { + throw new TypeError('SurfaceTaskOwner requires an object root'); + } + let state = states.get(root); + if (!state) { + state = createState(); + states.set(root, state); + } + return state; + }; + const cancelScheduled = (root, state) => { + const environment = environmentForRoot(root); + for (const task of [...state.scheduled]) { + state.scheduled.delete(task); + task.cancel?.(); + task.resolve({ status: 'revoked' }); + } + }; + const schedule = (root, kind, callback, options = {}) => { + const state = stateFor(root); + if (state.disposed) return Promise.reject(new Error('SurfaceTaskOwner root is disposed')); + const generation = state.generation; + const environment = environmentForRoot(root); + return new Promise((resolve, reject) => { + const task = { kind, handle: null, resolve, cancel: null }; + const invoke = timestamp => { + state.scheduled.delete(task); + if (state.disposed || generation !== state.generation) { + resolve({ status: 'revoked' }); + return; + } + const operation = Promise.resolve().then(() => callback(timestamp)); + state.running.add(operation); + operation.then( + value => resolve({ status: 'completed', value }), + reject, + ).finally(() => state.running.delete(operation)); + }; + if (kind === 'idle' && typeof environment.requestIdleCallback === 'function') { + task.handle = environment.requestIdleCallback(invoke, options); + task.cancel = () => environment.cancelIdleCallback?.(task.handle); + } else { + task.kind = 'frame'; + if (typeof environment.requestAnimationFrame === 'function') { + task.handle = environment.requestAnimationFrame(invoke); + task.cancel = () => environment.cancelAnimationFrame?.(task.handle); + } else { + task.handle = environment.setTimeout(() => invoke(Date.now()), 0); + task.cancel = () => environment.clearTimeout?.(task.handle); + } + } + state.scheduled.add(task); + }); + }; + + return Object.freeze({ + animationFrame(root, callback) { return schedule(root, 'frame', callback); }, + idle(root, callback, options) { return schedule(root, 'idle', callback, options); }, + revoke(root) { + const state = stateFor(root); + state.generation += 1; + cancelScheduled(root, state); + }, + async dispose(root) { + const state = stateFor(root); + if (!state.disposed) { + state.disposed = true; + state.generation += 1; + cancelScheduled(root, state); + } + await Promise.allSettled([...state.running]); + }, + }); +} diff --git a/modules/renderer/topicSelectionReadiness.js b/modules/renderer/topicSelectionReadiness.js new file mode 100644 index 000000000..521bc009e --- /dev/null +++ b/modules/renderer/topicSelectionReadiness.js @@ -0,0 +1,31 @@ +/** + * Owns the main renderer's topic-selection readiness handshake. + * + * The topic list is mounted before ChatManager is fully wired. This small + * capability lets the producer defer one user selection without reaching + * through ambient window fields; the renderer composition root remains the + * only owner that changes readiness or consumes the deferred operation. + */ +export function createTopicSelectionReadiness() { + let ready = false; + let pending = null; + + return Object.freeze({ + isReady: () => ready, + setReady(value) { + ready = value === true; + }, + defer(selection) { + if (!selection || typeof selection !== 'object') return; + pending = Object.freeze({ ...selection }); + }, + takePending() { + const selection = pending; + pending = null; + return selection; + }, + clearPending() { + pending = null; + }, + }); +} diff --git a/modules/renderer/ttsSurfaceOwner.js b/modules/renderer/ttsSurfaceOwner.js new file mode 100644 index 000000000..8876940f7 --- /dev/null +++ b/modules/renderer/ttsSurfaceOwner.js @@ -0,0 +1,138 @@ +/** Owns TTS playback state, preload subscriptions, and quiescent teardown for one Surface realm. */ +export function createTtsSurfaceOwner({ + subscribePlay, + subscribeStop, + createAudioContext, + decodeBase64, + updateSpeakingIndicator, + showError, + logger = console, +} = {}) { + if (typeof subscribePlay !== 'function' || typeof subscribeStop !== 'function') { + throw new TypeError('TtsSurfaceOwner requires play and stop subscription capabilities'); + } + if (typeof createAudioContext !== 'function' || typeof decodeBase64 !== 'function') { + throw new TypeError('TtsSurfaceOwner requires audio context and base64 capabilities'); + } + + let audioContext = null; + let currentSource = null; + let queue = []; + let playing = false; + let currentMessageId = null; + let sessionId = -1; + let mounted = false; + let disposed = false; + let generation = 0; + const unsubscribers = []; + const activeWork = new Set(); + + const setIndicator = (messageId, active) => { + if (messageId && typeof updateSpeakingIndicator === 'function') { + updateSpeakingIndicator(messageId, active); + } + }; + + const stopPlayback = () => { + generation += 1; + sessionId += 1; + queue = []; + playing = false; + if (currentSource) { + currentSource.onended = null; + try { currentSource.stop(); } catch (error) { logger.warn?.('[TTS] failed to stop source', error); } + currentSource = null; + } + setIndicator(currentMessageId, false); + currentMessageId = null; + }; + + const processQueue = async () => { + if (disposed || playing || queue.length === 0 || !audioContext) return; + playing = true; + const ownerGeneration = generation; + const { audioData, msgId } = queue.shift(); + if (currentMessageId !== msgId) { + setIndicator(currentMessageId, false); + currentMessageId = msgId; + setIndicator(currentMessageId, true); + } + + const work = (async () => { + try { + const bytes = decodeBase64(audioData); + const buffer = await audioContext.decodeAudioData(bytes); + if (disposed || !playing || ownerGeneration !== generation) return; + const source = audioContext.createBufferSource(); + currentSource = source; + source.buffer = buffer; + source.connect(audioContext.destination); + source.onended = () => { + if (disposed || ownerGeneration !== generation) return; + currentSource = null; + playing = false; + void processQueue(); + }; + source.start(0); + } catch (error) { + if (!disposed && ownerGeneration === generation) { + showError?.(`播放音频失败: ${error.message}`); + playing = false; + void processQueue(); + } + } + })(); + activeWork.add(work); + work.finally(() => activeWork.delete(work)); + await work; + }; + + const ensureAudioContext = () => { + if (disposed) return false; + if (audioContext) { + void processQueue(); + return true; + } + try { + audioContext = createAudioContext(); + void processQueue(); + return true; + } catch (error) { + logger.error?.('[TTS] failed to initialize AudioContext', error); + showError?.('无法初始化音频播放器。'); + return false; + } + }; + + const mount = () => { + if (disposed) throw new Error('TtsSurfaceOwner is disposed'); + if (mounted) return; + mounted = true; + unsubscribers.push(subscribePlay(({ audioData, msgId, sessionId: incomingSession }) => { + if (disposed || incomingSession < sessionId) return; + if (incomingSession > sessionId) { + stopPlayback(); + sessionId = incomingSession; + } + queue.push({ audioData, msgId }); + void processQueue(); + })); + unsubscribers.push(subscribeStop(() => stopPlayback())); + }; + + const dispose = async () => { + if (disposed) return; + disposed = true; + while (unsubscribers.length) { + try { unsubscribers.pop()?.(); } catch (error) { logger.warn?.('[TTS] unsubscribe failed', error); } + } + stopPlayback(); + await Promise.allSettled([...activeWork]); + if (typeof audioContext?.close === 'function') { + try { await audioContext.close(); } catch (error) { logger.warn?.('[TTS] AudioContext close failed', error); } + } + audioContext = null; + }; + + return Object.freeze({ mount, ensureAudioContext, dispose }); +} diff --git a/modules/renderer/visibilityOptimizer.js b/modules/renderer/visibilityOptimizer.js index 07e5998a0..6eb104c10 100644 --- a/modules/renderer/visibilityOptimizer.js +++ b/modules/renderer/visibilityOptimizer.js @@ -1,5 +1,44 @@ // modules/renderer/visibilityOptimizer.js +const visibilityOwnerByMessage = new WeakMap(); +const animateInterceptorByPrototype = new WeakMap(); + +function acquireElementAnimateInterceptor(elementPrototype) { + if (!elementPrototype || typeof elementPrototype.animate !== 'function') return () => {}; + + let realmState = animateInterceptorByPrototype.get(elementPrototype); + if (!realmState) { + const originalAnimate = elementPrototype.animate; + realmState = { originalAnimate, users: 0 }; + animateInterceptorByPrototype.set(elementPrototype, realmState); + elementPrototype.animate = function (keyframes, options) { + const animation = originalAnimate.call(this, keyframes, options); + const messageItem = this.closest?.('.message-item'); + visibilityOwnerByMessage.get(messageItem)?.captureWebAnimation(messageItem, animation); + return animation; + }; + } + realmState.users += 1; + + let released = false; + return () => { + if (released) return; + released = true; + realmState.users = Math.max(0, realmState.users - 1); + if (realmState.users === 0) { + if (elementPrototype.animate !== realmState.originalAnimate) { + elementPrototype.animate = realmState.originalAnimate; + } + animateInterceptorByPrototype.delete(elementPrototype); + } + }; +} + +/** Creates one visibility and animation scheduler owner for one renderer. */ +export function createVisibilityOptimizer() { +let publicApi = null; +let releaseElementAnimateInterceptor = null; + /** * 🎬 视界优化器 - 只暂停"会动的东西" * @@ -14,13 +53,13 @@ // 存储每个消息的动画状态 const messageAnimationStates = new WeakMap(); +const observedMessages = new Set(); +const scanTimers = new Map(); // 全局 Observer 实例 let visibilityObserver = null; let chatContainerRef = null; - -// 原始方法备份 -let originalElementAnimate = null; +let ownerWindow = null; // 配置 const CONFIG = { @@ -38,15 +77,17 @@ let batchTimer = null; /** * 初始化可见性优化器 */ -export function initializeVisibilityOptimizer(chatContainer) { - chatContainerRef = chatContainer; - +function initializeVisibilityOptimizer(chatContainer) { if (visibilityObserver) { + [...observedMessages].forEach(unobserveMessage); visibilityObserver.disconnect(); } - // 🔑 关键:注入全局拦截器 - injectGlobalInterceptors(); + chatContainerRef = chatContainer; + ownerWindow = chatContainer?.ownerDocument?.defaultView || window; + + releaseElementAnimateInterceptor?.(); + releaseElementAnimateInterceptor = acquireElementAnimateInterceptor(ownerWindow?.Element?.prototype); visibilityObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { @@ -77,39 +118,14 @@ export function initializeVisibilityOptimizer(chatContainer) { /** * 💉 注入全局拦截器 */ -function injectGlobalInterceptors() { - // 拦截 Web Animations API - if (!originalElementAnimate && typeof Element.prototype.animate === 'function') { - originalElementAnimate = Element.prototype.animate; - - Element.prototype.animate = function (keyframes, options) { - const animation = originalElementAnimate.call(this, keyframes, options); - - // 找到所属的消息气泡 - const messageItem = this.closest('.message-item'); - if (messageItem) { - const state = messageAnimationStates.get(messageItem); - if (state) { - if (!state.webAnimations.includes(animation)) { - state.webAnimations.push(animation); - } - - // 如果当前气泡已暂停,立即暂停新动画 - if (state.isPaused) { - // 延迟一帧确保动画初始化完成 - requestAnimationFrame(() => { - if (state.isPaused && animation.playState === 'running') { - animation.pause(); - } - }); - } - } - } - - return animation; - }; - - console.debug('[VisibilityOptimizer] Element.animate interceptor installed'); +function captureWebAnimation(messageItem, animation) { + const state = messageAnimationStates.get(messageItem); + if (!state) return; + if (!state.webAnimations.includes(animation)) state.webAnimations.push(animation); + if (state.isPaused) { + requestAnimationFrame(() => { + if (state.isPaused && animation.playState === 'running') animation.pause(); + }); } } @@ -135,8 +151,10 @@ function scheduleBatchProcess() { /** * 观察单个消息 */ -export function observeMessage(messageItem) { +function observeMessage(messageItem) { if (!visibilityObserver || !messageItem) return; + visibilityOwnerByMessage.set(messageItem, publicApi); + observedMessages.add(messageItem); // 初始化状态存储 if (!messageAnimationStates.has(messageItem)) { @@ -195,10 +213,16 @@ export function observeMessage(messageItem) { rememberMessageHeight(messageItem); // 🔑 延迟扫描,确保脚本已执行完毕 - setTimeout(() => { + const messageWindow = messageItem.ownerDocument?.defaultView || ownerWindow || window; + const existingScanTimer = scanTimers.get(messageItem); + if (existingScanTimer) existingScanTimer.window.clearTimeout(existingScanTimer.id); + const scanTimer = messageWindow.setTimeout(() => { + scanTimers.delete(messageItem); + if (!observedMessages.has(messageItem) || visibilityOwnerByMessage.get(messageItem) !== publicApi) return; scanAnimatedElements(messageItem); rememberMessageHeight(messageItem); }, CONFIG.scanDelay); + scanTimers.set(messageItem, { id: scanTimer, window: messageWindow }); } /** @@ -380,7 +404,7 @@ function cleanupFinishedAnimations(state) { /** * ⏸️ 暂停消息内的所有动画 */ -export function pauseMessageAnimations(messageItem) { +function pauseMessageAnimations(messageItem) { const state = messageAnimationStates.get(messageItem); if (!state || state.isPaused) return; @@ -485,7 +509,7 @@ function applyPauseToState(messageItem, state) { /** * ▶️ 恢复消息内的所有动画 */ -export function resumeMessageAnimations(messageItem) { +function resumeMessageAnimations(messageItem) { const state = messageAnimationStates.get(messageItem); if (!state) return; @@ -570,7 +594,7 @@ export function resumeMessageAnimations(messageItem) { /** * 📝 注册 anime.js 实例 */ -export function registerAnimeInstance(messageItem, animeInstance) { +function registerAnimeInstance(messageItem, animeInstance) { if (!messageItem || !animeInstance) return; const state = messageAnimationStates.get(messageItem); @@ -588,7 +612,7 @@ export function registerAnimeInstance(messageItem, animeInstance) { /** * 📝 注册 Three.js 上下文 */ -export function registerThreeContext(messageItem, context) { +function registerThreeContext(messageItem, context) { if (!messageItem || !context) return; const state = messageAnimationStates.get(messageItem); @@ -615,7 +639,7 @@ export function registerThreeContext(messageItem, context) { * @param {HTMLElement} messageItem * @param {Object} context - { canvas, pauseCallback?, resumeCallback? } */ -export function registerCanvasAnimation(messageItem, context) { +function registerCanvasAnimation(messageItem, context) { if (!messageItem || !context?.canvas) return; const state = messageAnimationStates.get(messageItem); @@ -649,7 +673,7 @@ export function registerCanvasAnimation(messageItem, context) { /** * ❓ 检查消息是否处于暂停状态 */ -export function isMessagePaused(messageItem) { +function isMessagePaused(messageItem) { if (!messageItem) return false; const state = messageAnimationStates.get(messageItem); return state ? state.isPaused : false; @@ -659,7 +683,7 @@ export function isMessagePaused(messageItem) { * 🔧 创建一个可暂停的 requestAnimationFrame 包装器 * 供 animation.js 在执行用户脚本时使用 */ -export function createPausableRAF(messageItem) { +function createPausableRAF(messageItem) { const wrappedRAF = (callback) => { const state = messageAnimationStates.get(messageItem); if (!state || !messageItem?.isConnected) { @@ -692,7 +716,7 @@ export function createPausableRAF(messageItem) { return wrappedRAF; } -export function createPausableTimerAPI(messageItem) { +function createPausableTimerAPI(messageItem) { const getState = () => messageAnimationStates.get(messageItem); const createTimerRecord = (type, callback, delay, args, repeat) => { @@ -796,12 +820,17 @@ export function createPausableTimerAPI(messageItem) { /** * 🗑️ 停止观察并清理消息 */ -export function unobserveMessage(messageItem) { +function unobserveMessage(messageItem) { if (visibilityObserver) { visibilityObserver.unobserve(messageItem); } const state = messageAnimationStates.get(messageItem); + const scanTimer = scanTimers.get(messageItem); + if (scanTimer) { + scanTimer.window.clearTimeout(scanTimer.id); + scanTimers.delete(messageItem); + } if (state) { // [新增] 断开 MutationObserver if (state.mutationObserver) { @@ -835,12 +864,14 @@ export function unobserveMessage(messageItem) { messageAnimationStates.delete(messageItem); } + if (visibilityOwnerByMessage.get(messageItem) === publicApi) visibilityOwnerByMessage.delete(messageItem); + observedMessages.delete(messageItem); pendingPause.delete(messageItem); pendingResume.delete(messageItem); } -export function isMessageInHotZone(messageItem, margin = 200) { +function isMessageInHotZone(messageItem, margin = 200) { if (!messageItem || !chatContainerRef || !messageItem.isConnected) return false; try { @@ -859,7 +890,7 @@ export function isMessageInHotZone(messageItem, margin = 200) { /** * 🔄 手动触发可见性检查 */ -export function recheckVisibility() { +function recheckVisibility() { if (!chatContainerRef) return; const containerRect = chatContainerRef.getBoundingClientRect(); @@ -881,17 +912,15 @@ export function recheckVisibility() { /** * 🛑 销毁优化器 */ -export function destroyVisibilityOptimizer() { +function destroyVisibilityOptimizer() { if (visibilityObserver) { + [...observedMessages].forEach(unobserveMessage); visibilityObserver.disconnect(); visibilityObserver = null; } - // 恢复原始的 Element.animate - if (originalElementAnimate) { - Element.prototype.animate = originalElementAnimate; - originalElementAnimate = null; - } + releaseElementAnimateInterceptor?.(); + releaseElementAnimateInterceptor = null; if (batchTimer) { clearTimeout(batchTimer); @@ -900,7 +929,31 @@ export function destroyVisibilityOptimizer() { pendingPause.clear(); pendingResume.clear(); + [...observedMessages].forEach(unobserveMessage); + scanTimers.forEach(timer => timer.window.clearTimeout(timer.id)); + scanTimers.clear(); chatContainerRef = null; + ownerWindow = null; console.debug('[VisibilityOptimizer] Destroyed'); -} \ No newline at end of file +} + +publicApi = Object.freeze({ + initializeVisibilityOptimizer, + observeMessage, + pauseMessageAnimations, + resumeMessageAnimations, + registerAnimeInstance, + registerThreeContext, + registerCanvasAnimation, + isMessagePaused, + createPausableRAF, + createPausableTimerAPI, + unobserveMessage, + isMessageInHotZone, + recheckVisibility, + destroyVisibilityOptimizer, + captureWebAnimation, +}); +return publicApi; +} diff --git a/modules/renderer/windowStreamRuntime.js b/modules/renderer/windowStreamRuntime.js new file mode 100644 index 000000000..ad3ff7d40 --- /dev/null +++ b/modules/renderer/windowStreamRuntime.js @@ -0,0 +1,59 @@ +import { createVcpStreamBridge } from '../chat/vcpStreamBridge.js'; +import { createMainChatStreamConsumer } from './mainChatStreamConsumer.js'; + +/** Owns the VCP stream bridge and projection capabilities for an auxiliary window. */ +export function createWindowStreamRuntime({ + streamProjection, + historyPersistence, + root, + getSelection, + getTopicId, + contextFilter, + getMessageContext = () => ({}), + dispatchTerminal, + afterPersist, + onSettled, + reportError, +} = {}) { + if (!streamProjection || !historyPersistence || !root || typeof contextFilter !== 'function') { + throw new TypeError('window stream runtime requires projection, persistence and context filter'); + } + const bridge = createVcpStreamBridge({ + reportError, + createConsumer: initialEvent => createMainChatStreamConsumer({ + ...initialEvent, + context: { ...getMessageContext(), ...(initialEvent.context || {}) }, + }, { + getSurfaceGeneration: () => 0, + start: message => streamProjection.startStreamingMessage(message), + append: (messageId, chunk, context) => streamProjection.appendStreamChunk(messageId, chunk, context), + projectTerminal: (messageId, finishReason, context, payload) => streamProjection.projectStreamTerminal(messageId, finishReason, context, payload), + persistTerminal: projected => historyPersistence.commit(projected), + dispatchTerminal, + reportError, + async afterPersist(value) { await afterPersist?.(value); }, + onSettled(value) { onSettled?.(value); }, + renderError: ({ event, context }) => { + if (!contextFilter(context)) return; + const error = event.outcome?.transport?.error?.message + || event.outcome?.transport?.error + || event.outcome?.persistence?.error?.message + || '未知流错误'; + const content = root.querySelector(`.message-item[data-message-id="${event.messageId}"]`)?.querySelector('.md-content'); + if (content) content.textContent = error; + }, + }), + }); + return Object.freeze({ + accept(event) { + if (!contextFilter(event?.context)) return false; + return bridge.accept(event); + }, + cancel(messageId, reason = 'window-close') { + return bridge.cancelOperation(messageId, reason); + }, + dispose() { return bridge.dispose(); }, + getSelection, + getTopicId, + }); +} diff --git a/modules/searchManager.js b/modules/searchManager.js index 4a89da9b0..c4a91ae4e 100644 --- a/modules/searchManager.js +++ b/modules/searchManager.js @@ -8,7 +8,8 @@ const CODE_FENCE_REGEX = /```\w*([\s\S]*?)```/g; const searchManager = { // --- Properties --- - electronAPI: null, + electronAPI: null, + chatRepository: null, uiHelper: null, chatManager: null, currentSelectedItemRef: null, @@ -28,7 +29,9 @@ const searchManager = { // --- Initialization --- init(dependencies) { console.log('[SearchManager] Initializing...'); - this.electronAPI = dependencies.electronAPI; + this.electronAPI = dependencies.electronAPI; + this.chatRepository = dependencies.chatRepository || null; + if (!this.chatRepository) throw new Error('SearchManager requires ChatRepository'); this.uiHelper = dependencies.uiHelper; this.chatManager = dependencies.modules.chatManager; this.currentSelectedItemRef = dependencies.refs.currentSelectedItemRef; @@ -315,9 +318,7 @@ const searchManager = { const historyReadPromises = topicsToFetch.map(info => { const { itemType, itemId, topicId } = info.context; - const promise = itemType === 'agent' - ? this.electronAPI.getChatHistory(itemId, topicId) - : this.electronAPI.getGroupChatHistory(itemId, topicId); + const promise = this.chatRepository.getHistory(itemId, itemType, topicId); return promise.then(history => { if (history && !history.error) { diff --git a/modules/settingsManager.js b/modules/settingsManager.js index 1405e59a1..cdb3d1597 100644 --- a/modules/settingsManager.js +++ b/modules/settingsManager.js @@ -44,6 +44,8 @@ const settingsManager = (() => { let uiHelper = null; let refs = {}; // To hold references to currentSelectedItem, etc. let mainRendererFunctions = {}; // To call back to renderer.js functions if needed + let messageRenderer = null; + let getGlobalSettings = () => ({}); // DOM Elements let agentSettingsContainer, groupSettingsContainer, selectItemPromptForSettings; @@ -406,8 +408,8 @@ const settingsManager = (() => { electronAPI.saveAvatarColor({ type: 'agent', id: agentId, color: avgColor }) .then((saveColorResult) => { if (saveColorResult && saveColorResult.success) { - if (refs.currentSelectedItemRef.get().id === agentId && refs.currentSelectedItemRef.get().type === 'agent' && window.messageRenderer) { - window.messageRenderer.setCurrentItemAvatarColor(avgColor); + if (refs.currentSelectedItemRef.get().id === agentId && refs.currentSelectedItemRef.get().type === 'agent' && messageRenderer) { + messageRenderer.setCurrentItemAvatarColor(avgColor); } } else { console.warn(`Failed to save agent ${agentId} avatar color:`, saveColorResult?.error); @@ -453,26 +455,24 @@ const settingsManager = (() => { console.error(`[SettingsManager] Failed to get updated agent config:`, updatedAgentConfig.error); uiHelper.showToastNotification(`无法刷新Agent配置: ${updatedAgentConfig.error}`, 'warning'); // 仍然更新名称,但不更新其他可能缺失的属性 - currentSelectedItem.name = newConfig.name; + refs.currentSelectedItemRef.set({ ...currentSelectedItem, name: newConfig.name }); selectedItemNameForSettingsSpan.textContent = newConfig.name; if (mainRendererFunctions.updateChatHeader) { mainRendererFunctions.updateChatHeader(`与 ${newConfig.name} 聊天中`); } } else if (updatedAgentConfig) { - currentSelectedItem.name = newConfig.name; - if (currentSelectedItem.config) { - currentSelectedItem.config = updatedAgentConfig; - } else { - Object.assign(currentSelectedItem, updatedAgentConfig); - } + const nextSelectedItem = currentSelectedItem.config + ? { ...currentSelectedItem, name: newConfig.name, config: updatedAgentConfig } + : { ...currentSelectedItem, ...updatedAgentConfig, name: newConfig.name }; + refs.currentSelectedItemRef.set(nextSelectedItem); // Update other UI parts via callbacks or direct calls if modules are passed in if (mainRendererFunctions.updateChatHeader) { mainRendererFunctions.updateChatHeader(`与 ${newConfig.name} 聊天中`); } - if (window.messageRenderer) { - window.messageRenderer.setCurrentItemAvatar(updatedAgentConfig.avatarUrl); - window.messageRenderer.setCurrentItemAvatarColor(updatedAgentConfig.avatarCalculatedColor || null); + if (messageRenderer) { + messageRenderer.setCurrentItemAvatar(updatedAgentConfig.avatarUrl); + messageRenderer.setCurrentItemAvatarColor(updatedAgentConfig.avatarCalculatedColor || null); } selectedItemNameForSettingsSpan.textContent = newConfig.name; } @@ -572,7 +572,7 @@ const settingsManager = (() => { async function populateTtsModels(currentPrimaryVoice, currentSecondaryVoice) { if (!agentTtsVoicePrimarySelect || !agentTtsVoiceSecondarySelect) return; - const globalSettings = window.globalSettings || {}; + const globalSettings = getGlobalSettings(); const isNetworkMode = globalSettings.voiceMode === 'network'; const commitOptions = (select, options, selectedValue = '') => { const current = [...select.options]; @@ -756,6 +756,11 @@ const settingsManager = (() => { // --- Public API --- return { + configureCapabilities: ({ settings = null } = {}) => { + getGlobalSettings = typeof settings?.get === 'function' + ? () => settings.get() || {} + : () => ({}); + }, init: (options) => { if (initialized) { console.warn('[SettingsManager] Ignored duplicate initialization.'); @@ -766,6 +771,7 @@ const settingsManager = (() => { uiHelper = options.uiHelper; refs = options.refs; mainRendererFunctions = options.mainRendererFunctions; + messageRenderer = options.messageRenderer || null; // DOM Elements (Always present) agentSettingsContainer = options.elements.agentSettingsContainer; @@ -971,7 +977,7 @@ const settingsManager = (() => { if (refreshTtsModelsBtn) { refreshTtsModelsBtn.addEventListener('click', async () => { - const isNetworkMode = (window.globalSettings || {}).voiceMode === 'network'; + const isNetworkMode = getGlobalSettings().voiceMode === 'network'; uiHelper.showToastNotification(isNetworkMode ? '正在刷新网络音色列表...' : '正在刷新语音模型...', 'info'); try { if (electronAPI.sovitsGetModels) { @@ -2128,11 +2134,10 @@ function setupParamsCollapsible() { return; } - if (currentSelectedItem.config) { - currentSelectedItem.config.uiCollapseStates = { ...collapseStates }; - } else { - currentSelectedItem.uiCollapseStates = { ...collapseStates }; - } + const nextSelectedItem = currentSelectedItem.config + ? { ...currentSelectedItem, config: { ...currentSelectedItem.config, uiCollapseStates: { ...collapseStates } } } + : { ...currentSelectedItem, uiCollapseStates: { ...collapseStates } }; + refs.currentSelectedItemRef.set(nextSelectedItem); } async function persistCollapseStatesForCurrentSelection() { diff --git a/modules/text-viewer.js b/modules/text-viewer.js index 82bfef0b0..738b4dfcf 100644 --- a/modules/text-viewer.js +++ b/modules/text-viewer.js @@ -1,5 +1,7 @@ -import * as emoticonFixer from './renderer/emoticonUrlFixer.js'; -import { domToCanvas, domToBlob } from '../vendor/modern-screenshot.js'; +import { createEmoticonUrlFixer } from './renderer/emoticonUrlFixer.js'; +import { domToCanvas, domToBlob } from '../vendor/modern-screenshot.js'; + +const emoticonFixer = createEmoticonUrlFixer(); document.addEventListener('DOMContentLoaded', async () => { const viewerAPI = window.utilityAPI || window.electronAPI; diff --git a/modules/topicListManager.js b/modules/topicListManager.js index 169a836e0..96d207a59 100644 --- a/modules/topicListManager.js +++ b/modules/topicListManager.js @@ -3,11 +3,17 @@ window.topicListManager = (() => { // --- Private Variables --- let topicListContainer; - let electronAPI; + let electronAPI; + let chatRepository; let currentSelectedItemRef; - let currentTopicIdRef; + let currentTopicIdRef; + let topicSelectionReadiness; let uiHelper; - let mainRendererFunctions; + let mainRendererFunctions; + let listenerOwner; + let uiManager; + let itemListManager; + let renderListenerDisposers = []; let wasSelectionListenerActive = false; // To store the state of the selection listener before dragging let topicListRenderGeneration = 0; let topicListScrollCleanup = null; @@ -21,7 +27,22 @@ window.topicListManager = (() => { const TOPIC_INITIAL_RENDER_COUNT = 40; const TOPIC_PROGRESSIVE_BATCH_SIZE = 30; - const TOPIC_LOAD_MORE_THRESHOLD_PX = 320; + const TOPIC_LOAD_MORE_THRESHOLD_PX = 320; + + const addListener = (target, type, handler, options) => { + if (listenerOwner) return listenerOwner.add(target, type, handler, options); + target?.addEventListener?.(type, handler, options); + return true; + }; + + function addRenderListener(target, type, handler, options) { + target?.addEventListener?.(type, handler, options); + renderListenerDisposers.push(() => target?.removeEventListener?.(type, handler, options)); + } + + function disposeRenderListeners() { + renderListenerDisposers.splice(0).reverse().forEach(dispose => dispose()); + } /** * Initializes the TopicListManager module. @@ -32,24 +53,34 @@ window.topicListManager = (() => { console.error('[TopicListManager] Missing required DOM element: topicListContainer.'); return; } - if (!config.electronAPI || !config.refs || !config.uiHelper || !config.mainRendererFunctions) { + if (!config.electronAPI || !config.refs || !config.uiHelper || !config.mainRendererFunctions || !config.topicSelectionReadiness) { console.error('[TopicListManager] Missing required configuration parameters.'); return; } topicListContainer = config.elements.topicListContainer; - electronAPI = config.electronAPI; + electronAPI = config.electronAPI; + chatRepository = config.chatRepository || null; + if (!chatRepository) throw new Error('TopicListManager requires ChatRepository'); currentSelectedItemRef = config.refs.currentSelectedItemRef; - currentTopicIdRef = config.refs.currentTopicIdRef; - uiHelper = config.uiHelper; - mainRendererFunctions = config.mainRendererFunctions; + currentTopicIdRef = config.refs.currentTopicIdRef; + topicSelectionReadiness = config.topicSelectionReadiness; + uiHelper = config.uiHelper; + mainRendererFunctions = config.mainRendererFunctions; + listenerOwner = config.listenerOwner || null; + uiManager = config.uiManager || null; + itemListManager = config.itemListManager || null; // 设置鼠标快捷键 setupMouseShortcuts(); setupNextUiTopicTools(); - console.log('[TopicListManager] Initialized successfully.'); - } + console.log('[TopicListManager] Initialized successfully.'); + } + + function getHistory(itemId, itemType, topicId) { + return chatRepository.getHistory(itemId, itemType, topicId); + } function hasUserParticipation(history) { return Array.isArray(history) && history.some(message => @@ -151,9 +182,7 @@ window.topicListManager = (() => { const topicsWithUnreadState = await Promise.all(topics.map(async topic => { let history = []; try { - history = currentSelectedItem.type === 'group' - ? await electronAPI.getGroupChatHistory(currentSelectedItem.id, topic.id) - : await electronAPI.getChatHistory(currentSelectedItem.id, topic.id); + history = await getHistory(currentSelectedItem.id, currentSelectedItem.type, topic.id); } catch (error) { console.warn(`[TopicListManager] 读取话题 ${topic.id} 的未读状态失败:`, error); } @@ -222,8 +251,9 @@ window.topicListManager = (() => { return 0; // 不显示 } - function cleanupProgressiveTopicRendering() { - topicListRenderGeneration++; + function cleanupProgressiveTopicRendering() { + topicListRenderGeneration++; + disposeRenderListeners(); if (typeof topicListScrollCleanup === 'function') { topicListScrollCleanup(); topicListScrollCleanup = null; @@ -277,11 +307,7 @@ window.topicListManager = (() => { li.dataset.countLoading = 'true'; let historyPromise; - if (itemType === 'agent') { - historyPromise = electronAPI.getChatHistory(itemId, topicId); - } else if (itemType === 'group') { - historyPromise = electronAPI.getGroupChatHistory(itemId, topicId); - } + historyPromise = getHistory(itemId, itemType, topicId); if (!historyPromise) { messageCountSpan.textContent = 'N/A'; @@ -381,7 +407,7 @@ window.topicListManager = (() => { const observer = ensureTopicCountObserver(); observer.observe(li); - li.addEventListener('click', async () => { + addRenderListener(li, 'click', async () => { if (isManageMode) { toggleTopicSelection(topic.id); return; @@ -391,12 +417,12 @@ window.topicListManager = (() => { return; } - if (window.__vcpRendererReady === false) { - window.__vcpPendingTopicSelection = { - itemId: currentSelectedItem.id, - itemType: currentSelectedItem.type, - topicId: topic.id, - }; + if (!topicSelectionReadiness.isReady()) { + topicSelectionReadiness.defer({ + itemId: currentSelectedItem.id, + itemType: currentSelectedItem.type, + topicId: topic.id, + }); if (uiHelper && uiHelper.showToastNotification) { uiHelper.showToastNotification('正在初始化界面,稍后自动打开该话题', 'info'); } @@ -413,7 +439,7 @@ window.topicListManager = (() => { } }); - li.addEventListener('contextmenu', (e) => { + addRenderListener(li, 'contextmenu', (e) => { e.preventDefault(); if (isManageMode) return; showTopicContextMenu(e, li, itemConfigFull, topic, currentSelectedItem.type); @@ -662,8 +688,8 @@ window.topicListManager = (() => { if (inputElement.dataset.topicSearchBound === 'true') return; inputElement.dataset.topicSearchBound = 'true'; - inputElement.addEventListener('input', filterTopicList); - inputElement.addEventListener('keydown', (event) => { + addListener(inputElement, 'input', filterTopicList); + addListener(inputElement, 'keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); filterTopicList(); @@ -701,10 +727,10 @@ window.topicListManager = (() => { else if (document.activeElement === searchInput) searchButton.focus(); }; - createButton.addEventListener('click', () => { + addListener(createButton, 'click', () => { document.getElementById('currentAgentSettingsBtn')?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); - createButton.addEventListener('contextmenu', (event) => { + addListener(createButton, 'contextmenu', (event) => { event.preventDefault(); document.getElementById('currentAgentSettingsBtn')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, @@ -712,18 +738,18 @@ window.topicListManager = (() => { clientY: event.clientY })); }); - manageButton.addEventListener('click', () => setManageMode(!isManageMode)); - searchButton.addEventListener('click', () => setSearchMode(true, false)); - searchCloseButton.addEventListener('click', () => setSearchMode(false, true)); - searchInput.addEventListener('keydown', (event) => { + addListener(manageButton, 'click', () => setManageMode(!isManageMode)); + addListener(searchButton, 'click', () => setSearchMode(true, false)); + addListener(searchCloseButton, 'click', () => setSearchMode(false, true)); + addListener(searchInput, 'keydown', (event) => { if (event.key === 'Escape') { event.preventDefault(); setSearchMode(false, true); } }); - selectAllButton?.addEventListener('click', toggleSelectAllDisplayedTopics); - deleteButton?.addEventListener('click', deleteSelectedTopics); - exitButton?.addEventListener('click', () => setManageMode(false)); + if (selectAllButton) addListener(selectAllButton, 'click', toggleSelectAllDisplayedTopics); + if (deleteButton) addListener(deleteButton, 'click', deleteSelectedTopics); + if (exitButton) addListener(exitButton, 'click', () => setManageMode(false)); syncManageUi(); } @@ -839,9 +865,11 @@ window.topicListManager = (() => { } if (activeTopicDeleted) { - mainRendererFunctions.handleTopicDeletion(remainingTopics, { - id: currentSelectedItem.id, - type: currentSelectedItem.type + mainRendererFunctions.handleTopicDeletion(remainingTopics, { + id: currentSelectedItem.id, + type: currentSelectedItem.type, + deletedTopicIds: topicsToDelete.map(topic => topic.id), + fallbackTopicId: topicsToDelete.some(topic => topic.id === activeTopicId) ? null : activeTopicId, }); } @@ -1070,8 +1098,8 @@ window.topicListManager = (() => { ); loadTopicList(); // 刷新列表 // 同时刷新助手列表以更新计数 - if (window.itemListManager) { - window.itemListManager.loadItems(); + if (itemListManager) { + itemListManager.loadItems(); } } else { uiHelper.showToastNotification(`操作失败: ${result.error}`, 'error'); @@ -1112,9 +1140,11 @@ window.topicListManager = (() => { if (result && result.success) { if (currentTopicIdRef.get() === topic.id) { - mainRendererFunctions.handleTopicDeletion(result.remainingTopics, { - id: itemFullConfig.id, - type: itemType + mainRendererFunctions.handleTopicDeletion(result.remainingTopics, { + id: itemFullConfig.id, + type: itemType, + topicId: topic.id, + fallbackTopicId: itemFullConfig.topics?.find(candidate => candidate.id !== topic.id)?.id || null, }); } loadTopicList(); @@ -1326,9 +1356,7 @@ window.topicListManager = (() => { console.log(`[TopicListManager] Exporting full raw topic: ${topicName} (ID: ${topicId})`); try { - const history = itemType === 'group' - ? await electronAPI.getGroupChatHistory(itemFullConfig.id, topicId) - : await electronAPI.getChatHistory(itemFullConfig.id, topicId); + const history = await getHistory(itemFullConfig.id, itemType, topicId); if (!Array.isArray(history)) { throw new Error(history?.error || '无法读取话题历史'); @@ -1391,7 +1419,7 @@ window.topicListManager = (() => { let lastLeftClickTime = 0; // 双击左键:进入设置页面 - topicsContainer.addEventListener('click', (e) => { + addListener(topicsContainer, 'click', (e) => { if (isManageMode) return; if (e.target.closest('button, input, .topics-header-container, .next-ui-topic-manage-panel')) return; if (e.button === 0) { // 左键 @@ -1404,8 +1432,8 @@ window.topicListManager = (() => { e.stopPropagation(); // 切换到设置页面 - if (window.uiManager && typeof window.uiManager.switchToTab === 'function') { - window.uiManager.switchToTab('settings'); + if (uiManager && typeof uiManager.switchToTab === 'function') { + uiManager.switchToTab('settings'); } else { console.warn('[TopicListManager] uiManager不可用,无法切换到设置页面'); } @@ -1416,18 +1444,18 @@ window.topicListManager = (() => { }); // 中键点击:返回助手页面 - topicsContainer.addEventListener('auxclick', (e) => { + addListener(topicsContainer, 'auxclick', (e) => { if (e.button === 1) { // 中键 console.log('[TopicListManager] 检测到中键点击,返回助手页面'); e.preventDefault(); e.stopPropagation(); // 切换到助手页面 - if (window.uiManager && typeof window.uiManager.switchToTab === 'function') { - window.uiManager.switchToTab('agents'); + if (uiManager && typeof uiManager.switchToTab === 'function') { + uiManager.switchToTab('agents'); // 重置助手页面的鼠标事件状态,确保双击功能正常工作 - if (window.itemListManager && typeof window.itemListManager.resetMouseEventStates === 'function') { - window.itemListManager.resetMouseEventStates(); + if (itemListManager && typeof itemListManager.resetMouseEventStates === 'function') { + itemListManager.resetMouseEventStates(); } } else { console.warn('[TopicListManager] uiManager不可用,无法切换到助手页面'); @@ -1436,7 +1464,7 @@ window.topicListManager = (() => { }); // 防止中键点击的默认行为 - topicsContainer.addEventListener('mousedown', (e) => { + addListener(topicsContainer, 'mousedown', (e) => { if (e.button === 1) { // 中键 e.preventDefault(); } @@ -1445,12 +1473,25 @@ window.topicListManager = (() => { console.log('[TopicListManager] 鼠标快捷键设置完成'); } - // --- Public API --- + function dispose() { + cleanupProgressiveTopicRendering(); + closeTopicContextMenu(); + selectedTopicIds.clear(); + displayedTopics = []; + availableTopics = []; + currentItemConfig = null; + managedItemKey = ''; + topicListContainer = null; + listenerOwner = null; + } + + // --- Public API --- return { init, loadTopicList, setupTopicSearch, showTopicContextMenu, - setupMouseShortcuts + setupMouseShortcuts, + dispose }; })(); diff --git a/modules/ui-system/appearance-studio.js b/modules/ui-system/appearance-studio.js index e94aa9c2c..411db1a40 100644 --- a/modules/ui-system/appearance-studio.js +++ b/modules/ui-system/appearance-studio.js @@ -3,6 +3,29 @@ const LifecycleScope = window.VCPLifecycle?.LifecycleScope; const moduleScope = LifecycleScope ? new LifecycleScope('next:appearance-studio-controller') : null; + let capabilities = Object.create(null); + window.addEventListener('vcp-appearance-studio-configure', event => { + capabilities = Object.freeze({ ...(event.detail || {}) }); + }, { once: true }); + window.addEventListener('vcp-appearance-studio-dispose', event => { + if (destroyPromise) { + event.detail.promise = destroyPromise; + return; + } + destroyed = true; + destroyPromise = (async () => { + await close({ rollback: true }); + await moduleScope?.dispose('appearance-controller-destroyed'); + })(); + event.detail.promise = destroyPromise; + }, { once: true }); + const service = (name, fallback) => capabilities[name] || fallback?.(); + const getSettings = () => service('settings', null)?.get?.() || {}; + const replaceSettings = patch => service('settings', null)?.replace?.(patch); + const getAppearance = () => service('appearance', () => window.VCPAppearance); + const getUiManager = () => service('uiManager', () => window.uiManager); + const getPresentationMode = mode => service('presentation', null)?.normalize?.(mode); + const applyPresentation = (...args) => service('presentation', null)?.apply?.(...args); const DEFAULT_HOME_TAGLINE = '语义级打穿 AI、UI/UX、APP 与人类想象力的边界'; const CANONICAL_UI_MODE = 'next'; @@ -287,9 +310,8 @@ if (!normalizedMode) return false; const { persist = true, source = 'appearance-theme-control' } = options; const effectiveTheme = effectiveThemeForMode(normalizedMode); - window.globalSettings = window.globalSettings || {}; - window.globalSettings.currentThemeMode = normalizedMode; - window.uiManager?.applyTheme?.(effectiveTheme); + replaceSettings({ currentThemeMode: normalizedMode }); + getUiManager()?.applyTheme?.(effectiveTheme); if (persist) { if (normalizedMode === 'system') api()?.setThemeMode?.('system'); else api()?.setTheme?.(normalizedMode); @@ -297,20 +319,20 @@ syncAccountMenuValue(); syncSettingsSummary(); window.dispatchEvent(new CustomEvent('global-settings-updated', { - detail: { settings: window.globalSettings, source } + detail: { settings: getSettings(), source } })); return true; } function readState() { - const settings = window.globalSettings || {}; + const settings = getSettings(); const themeMode = THEME_MODES.has(settings.currentThemeMode) ? settings.currentThemeMode : readEffectiveTheme(); return { - profile: window.VCPAppearance?.normalize(settings.appearanceProfile, CANONICAL_UI_MODE) + profile: getAppearance()?.normalize(settings.appearanceProfile, CANONICAL_UI_MODE) || clone(PRESETS.balanced.profile), - presentation: window.normalizeChatPresentationMode?.(settings.chatPresentationMode) || 'bubble', + presentation: getPresentationMode(settings.chatPresentationMode) || 'bubble', messageWidth: settings.enableWideChatLayout === true ? 'wide' : 'normal', homeVisual: settings.showHomeVisualBrand === false ? 'hidden' : 'shown', homeTagline: settings.showHomeVisualTagline === false ? 'hidden' : 'shown', @@ -328,9 +350,9 @@ ? source.themeFileName : (typeof base.themeFileName === 'string' ? base.themeFileName : null); return { - profile: window.VCPAppearance?.normalize(source.profile || base.profile, CANONICAL_UI_MODE) + profile: getAppearance()?.normalize(source.profile || base.profile, CANONICAL_UI_MODE) || clone(base.profile), - presentation: window.normalizeChatPresentationMode?.(source.presentation || base.presentation) + presentation: getPresentationMode(source.presentation || base.presentation) || base.presentation, messageWidth: source.messageWidth === 'wide' || source.messageWidth === 'normal' ? source.messageWidth @@ -1002,7 +1024,7 @@ if (!draft) return; const generation = ++previewGeneration; if (generation !== previewGeneration || !draft) return; - window.VCPAppearance?.apply(draft.profile, { + getAppearance()?.apply(draft.profile, { uiMode: CANONICAL_UI_MODE, cache: false, source: 'appearance-studio-preview' @@ -1011,9 +1033,9 @@ applyHomeVisual(draft.homeVisual); applyHomeTagline(draft.homeTagline, draft.homeTaglineText); if (!options.appearanceOnly) { - window.uiManager?.applyTheme?.(effectiveThemeForMode(draft.themeMode)); + getUiManager()?.applyTheme?.(effectiveThemeForMode(draft.themeMode)); previewThemeFile(draft.themeFileName); - await window.applyChatPresentationMode?.(draft.presentation, { + await applyPresentation(draft.presentation, { persist: false, preserveScroll: true, notify: false, @@ -1029,9 +1051,9 @@ async function restoreSnapshot() { if (!snapshot) return; previewGeneration += 1; - if ((window.VCPAppearance?.getRevision?.() || 0) !== snapshotRevision) return; + if ((getAppearance()?.getRevision?.() || 0) !== snapshotRevision) return; removeThemePreview(); - window.VCPAppearance?.apply(snapshot.profile, { + getAppearance()?.apply(snapshot.profile, { uiMode: CANONICAL_UI_MODE, cache: false, source: 'appearance-studio-rollback' @@ -1039,8 +1061,8 @@ document.body.classList.toggle('chat-wide-layout', snapshot.messageWidth === 'wide'); applyHomeVisual(snapshot.homeVisual); applyHomeTagline(snapshot.homeTagline, snapshot.homeTaglineText); - window.uiManager?.applyTheme?.(effectiveThemeForMode(snapshot.themeMode)); - await window.applyChatPresentationMode?.(snapshot.presentation, { + getUiManager()?.applyTheme?.(effectiveThemeForMode(snapshot.themeMode)); + await applyPresentation(snapshot.presentation, { persist: false, preserveScroll: true, notify: false, @@ -1108,7 +1130,7 @@ if (!result?.success) throw new Error(result?.error || '设置保存失败'); settingsPersisted = true; - Object.assign(window.globalSettings || {}, { + replaceSettings({ appearanceProfile: nextState.profile, chatPresentationMode: nextState.presentation, enableWideChatLayout: nextState.messageWidth === 'wide', @@ -1117,7 +1139,7 @@ homeVisualTagline: nextState.homeTaglineText, currentThemeMode: nextState.themeMode }); - await window.applyChatPresentationMode?.(nextState.presentation, { + await applyPresentation(nextState.presentation, { persist: false, preserveScroll: true, notify: false, @@ -1126,28 +1148,28 @@ // Commit the appearance revision only after all fallible local // projections have succeeded. Otherwise rollback mistakes our // own partial commit for a newer external settings revision. - window.VCPAppearance?.commit(nextState.profile, { + getAppearance()?.commit(nextState.profile, { uiMode: CANONICAL_UI_MODE, source: 'appearance-studio-save' }); if (nextState.themeMode === 'system') { api()?.setThemeMode?.('system'); - window.uiManager?.applyTheme?.(effectiveThemeForMode('system')); + getUiManager()?.applyTheme?.(effectiveThemeForMode('system')); } else { api()?.setTheme?.(nextState.themeMode); - window.uiManager?.applyTheme?.(nextState.themeMode); + getUiManager()?.applyTheme?.(nextState.themeMode); } if (nextState.themeFileName && nextState.themeFileName !== snapshot.themeFileName) { api()?.applyTheme?.(nextState.themeFileName); } syncSettingsControls(); snapshot = clone(nextState); - snapshotRevision = window.VCPAppearance?.getRevision?.() || snapshotRevision; + snapshotRevision = getAppearance()?.getRevision?.() || snapshotRevision; syncAccountMenuValue(nextState); window.dispatchEvent(new CustomEvent('global-settings-updated', { - detail: { settings: window.globalSettings, source: 'appearance-studio' } + detail: { settings: getSettings(), source: 'appearance-studio' } })); - window.uiHelperFunctions?.showToastNotification?.('外观与布局已应用。', 'success'); + service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.('外观与布局已应用。', 'success'); await close({ rollback: false }); } catch (error) { if (settingsPersisted && persistedSnapshot) { @@ -1156,12 +1178,12 @@ if (!rollbackResult?.success) throw new Error(rollbackResult?.error || '设置回写失败'); } catch (rollbackError) { console.error('[AppearanceStudio] Failed to restore persisted settings:', rollbackError); - window.uiHelperFunctions?.showToastNotification?.( + service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.( `外观保存失败,且磁盘设置恢复失败:${rollbackError.message}`, 'error' ); } - Object.assign(window.globalSettings || {}, { + replaceSettings({ ...persistedSnapshot, appearanceProfile: clone(persistedSnapshot.appearanceProfile) }); @@ -1169,7 +1191,7 @@ await restoreSnapshot(); draft = clone(snapshot); syncControls(); - window.uiHelperFunctions?.showToastNotification?.(`外观与布局保存失败:${error.message}`, 'error'); + service('uiHelper', () => window.uiHelperFunctions)?.showToastNotification?.(`外观与布局保存失败:${error.message}`, 'error'); } finally { setBusy(false); } @@ -1286,7 +1308,7 @@ if (!openScope && moduleScope) openScope = moduleScope.child('next:appearance-studio-open'); acquireStudioOverlay(); snapshot = readState(); - snapshotRevision = window.VCPAppearance?.getRevision?.() || 0; + snapshotRevision = getAppearance()?.getRevision?.() || 0; draft = normalizeState(options.initialState, snapshot); currentSurface.root.hidden = false; document.body.classList.add('vcp-appearance-studio-open'); @@ -1392,7 +1414,7 @@ api()?.openThemesWindow?.(); } else if (target.dataset.studioAction === 'settings') { if (!await requestClose()) return; - window.uiHelperFunctions?.openModal?.('globalSettingsModal'); + service('uiHelper', () => window.uiHelperFunctions)?.openModal?.('globalSettingsModal'); } } @@ -1501,7 +1523,7 @@ else requestAnimationFrame(bindSettingsSummary); }); - window.VCPAppearanceStudio = Object.freeze({ + Object.defineProperty(window, 'VCPAppearanceStudio', { value: Object.freeze({ PRESETS, open, close, @@ -1511,14 +1533,5 @@ syncAccountMenuValue, syncSettingsSummary, setThemeMode, - destroy() { - if (destroyPromise) return destroyPromise; - destroyed = true; - destroyPromise = (async () => { - await close({ rollback: true }); - await moduleScope?.dispose('appearance-controller-destroyed'); - })(); - return destroyPromise; - } - }); + }), writable: false, configurable: false }); })(); diff --git a/modules/ui-system/ask-nova-modal.js b/modules/ui-system/ask-nova-modal.js index 1c22ef92b..1f95936c8 100644 --- a/modules/ui-system/ask-nova-modal.js +++ b/modules/ui-system/ask-nova-modal.js @@ -1,4 +1,8 @@ -import { fixEmoticonUrl } from '../renderer/emoticonUrlFixer.js'; +import { createEmoticonUrlFixer } from '../renderer/emoticonUrlFixer.js'; + +// Ask Nova is a standalone surface; keep its sticker repair state local to +// this module instead of importing a mutable renderer singleton. +const emoticonUrlFixer = createEmoticonUrlFixer(); const NOVA_STICKER_CATEGORY = 'Nova表情包'; const NOVA_STICKER_PLACEHOLDER_REGEX = /\[表情包:([^\]\r\n]{1,100})\]/g; @@ -137,7 +141,7 @@ function replaceStickerPlaceholders(root, stickerMap, outputDocument) { if (!originalUrl || !isSafeExternalUrl(originalUrl)) continue; replacement.append(outputDocument.createTextNode(text.slice(lastIndex, match.index))); const image = outputDocument.createElement('img'); - const fixedUrl = fixEmoticonUrl(originalUrl); + const fixedUrl = emoticonUrlFixer.fixEmoticonUrl(originalUrl); image.src = isSafeExternalUrl(fixedUrl) ? fixedUrl : originalUrl; image.alt = stickerName; image.title = stickerName; diff --git a/modules/ui-system/interactive-chat-app.js b/modules/ui-system/interactive-chat-app.js new file mode 100644 index 000000000..249cb2c7c --- /dev/null +++ b/modules/ui-system/interactive-chat-app.js @@ -0,0 +1,99 @@ +import { register } from './next-ui-apps.js'; +import { createChatSurface } from '../chat/chatSurface.js'; +import { createChatOperations } from '../chat/chatOperation.js'; + +function mountInteractiveChat(container, context = {}) { + const repository = context.chat?.repository; + const chatSnapshot = context.chat?.getSnapshot?.() || null; + const createRenderer = context.chat?.createRenderer; + const chatManager = context.chat?.manager; + const scope = context.scope?.child?.('next:interactive-chat') || null; + container.innerHTML = `

    独立聊天

    当前话题

    `; + const root = container.querySelector('.vcp-standalone-chat__messages'); + const form = container.querySelector('form'); + const input = form.querySelector('textarea'); + const status = form.querySelector('[role="status"]'); + let activeOperation = null; + let operationReady = Promise.resolve(null); + let publishOperation = null; + let submitInteractiveContent = null; + if (!repository || typeof createRenderer !== 'function' || !chatSnapshot || !chatManager) { + container.querySelector('[role="status"]').textContent = '聊天能力尚未就绪'; + return async () => { scope?.dispose?.('interactive-chat-unavailable'); container.replaceChildren(); }; + } + const rendererOwner = createRenderer({ + root, + mode: 'interactive', + conversation: chatSnapshot, + handleSendMessage: text => submitInteractiveContent?.(text), + }); + const renderer = rendererOwner.renderer; + const operations = createChatOperations({ + send: async request => { + operationReady = new Promise(resolve => { publishOperation = resolve; }); + try { + return await chatManager.sendMessage({ + ...request, + conversation: rendererOwner.conversation, + awaitTerminal: true, + onOperation(operation) { + activeOperation = operation; + publishOperation?.(operation); + publishOperation = null; + }, + }); + } finally { + publishOperation?.(null); + publishOperation = null; + activeOperation = null; + } + }, + cancel: async () => { + const operation = activeOperation || await operationReady; + return operation?.cancel?.('interactive-user-cancel') || false; + } + }); + const surface = createChatSurface({ root, renderer, repository, focusTarget: input, mode: 'interactive', operations, disposeRenderer: () => rendererOwner.dispose(), conversation: rendererOwner.conversation }); + const onSubmit = async event => { + event.preventDefault(); + const content = input.value.trim(); + if (!content) return; + form.setAttribute('aria-busy', 'true'); + input.disabled = true; + try { + const result = await surface.sendMessage({ content, attachments: [], input, domRenderer: surface.renderer, propagateError: true }); + input.value = ''; + const terminalType = result?.terminal?.event?.type; + if (terminalType === 'cancelled' || terminalType === 'discarded') status.textContent = '已取消'; + else if (terminalType === 'failed') { + const error = result.terminal.event.outcome?.transport?.error + || result.terminal.event.outcome?.persistence?.error + || '流式连接中断'; + status.textContent = `发送失败:${error?.message || error}`; + } else status.textContent = '已发送'; + } catch (error) { + status.textContent = `发送失败:${error.message}`; + } finally { + form.removeAttribute('aria-busy'); + input.disabled = false; + input.focus(); + } + }; + submitInteractiveContent = text => { + input.value = String(text || ''); + form.requestSubmit(); + }; + const onCancel = async () => { + const cancelled = await surface.cancelMessage(); + if (cancelled) status.textContent = '已取消'; + }; + form.addEventListener('submit', onSubmit); + form.querySelector('[data-action="cancel"]').addEventListener('click', onCancel); + const load = chatSnapshot.selectedItem?.id && chatSnapshot.topicId + ? surface.loadHistory(chatSnapshot.selectedItem.id, chatSnapshot.selectedItem.type, chatSnapshot.topicId, { initialBatch: 5, batchSize: 10, batchDelay: 80 }) + : Promise.resolve({ stale: false, history: [] }); + load.catch(error => { status.textContent = `加载失败:${error.message}`; }); + return async () => { submitInteractiveContent = null; form.removeEventListener('submit', onSubmit); form.querySelector('[data-action="cancel"]').removeEventListener('click', onCancel); await surface.dispose(); scope?.dispose?.('interactive-chat-unmounted'); container.replaceChildren(); }; +} + +register({ id: 'standalone-chat-compose', title: '独立聊天', icon: 'message-circle', kind: 'internal', discoverable: false, mount: mountInteractiveChat }); diff --git a/modules/ui-system/lifecycle-inspector.js b/modules/ui-system/lifecycle-inspector.js index 893086e74..0fe6acf6a 100644 --- a/modules/ui-system/lifecycle-inspector.js +++ b/modules/ui-system/lifecycle-inspector.js @@ -3,6 +3,7 @@ 'use strict'; if (!globalObject || globalObject.VCPLifecycleInspector) return; const transitionHistory = []; + let streamDiagnosticsProvider = null; const MAX_HISTORY = 30; const record = event => { const detail = event.detail || {}; @@ -30,7 +31,7 @@ contributions: globalObject.VCPContributions?.diagnostics?.snapshot?.() || null, states: Object.freeze(globalObject.VCPStateChannels?.diagnostics?.() || []), shell: globalObject.VCPNextShellController?.getDiagnostics?.() || null, - streams: globalObject.streamManager?.getDiagnostics?.() || null, + streams: streamDiagnosticsProvider?.() || null, performance: Object.freeze(globalObject.VCPPerformance?.snapshot?.() || []), transitions: Object.freeze([...transitionHistory]), }); @@ -47,5 +48,13 @@ }); } - globalObject.VCPLifecycleInspector = Object.freeze({ snapshot, snapshotMain }); + function setStreamDiagnosticsProvider(provider) { + if (typeof provider !== 'function') throw new TypeError('Stream diagnostics provider must be a function.'); + if (streamDiagnosticsProvider && streamDiagnosticsProvider !== provider) { + throw new Error('Stream diagnostics provider is already registered.'); + } + streamDiagnosticsProvider = provider; + } + + globalObject.VCPLifecycleInspector = Object.freeze({ snapshot, snapshotMain, setStreamDiagnosticsProvider }); })(typeof window !== 'undefined' ? window : null); diff --git a/modules/ui-system/next-shell/launchpad-controller.js b/modules/ui-system/next-shell/launchpad-controller.js index be556a935..97eb5c935 100644 --- a/modules/ui-system/next-shell/launchpad-controller.js +++ b/modules/ui-system/next-shell/launchpad-controller.js @@ -85,7 +85,7 @@ grid.append(button); }); listenKeydown(grid); - this.getInternalApps().forEach(app => { + this.getInternalApps().filter(app => app.discoverable !== false).forEach(app => { const button = this.document.createElement('button'); button.type = 'button'; button.className = 'next-ui-app-item next-ui-internal-app-item'; diff --git a/modules/ui-system/next-shell/next-shell-controller.js b/modules/ui-system/next-shell/next-shell-controller.js index c9a7630da..3875abb6f 100644 --- a/modules/ui-system/next-shell/next-shell-controller.js +++ b/modules/ui-system/next-shell/next-shell-controller.js @@ -19,6 +19,7 @@ let pendingTabRestore = null; let teardownPromise = null; let overlayCoordinator = null; + let chatCapabilities = null; let releaseWindowState = null; let tabOperationId = 0; let tabOperationRevision = 0; @@ -224,13 +225,14 @@ close: () => closeView(viewId), activate: () => setView(viewId), feedback: window.VCPUI?.feedback, - scope: viewScope + scope: viewScope, + chat: chatCapabilities, }); let disposer = null; try { disposer = app.mount(container, context) || null; } catch (error) { - console.error(`[NextUiApps] Failed to mount ${app.id}:`, error); + console.error(`[NextUiApps] Failed to mount ${app.id}:`, error?.stack || error?.message || error); container.textContent = `应用加载失败:${error.message}`; } if (viewScope) { @@ -315,9 +317,29 @@ return; } const viewId = `app:${app.id}`; - if (appTabHost.views.has(viewId)) { - setView(viewId); - return; + const existingView = appTabHost.views.get(viewId); + if (existingView) { + // A child WebContents can close before its asynchronous state + // notification removes the renderer tab. Reusing that stale tab + // leaves the user looking at an empty native Surface and prevents + // a new session from being created. Main is the session authority: + // reconcile before treating an existing renderer view as reusable. + try { + const authoritative = await embeddedAppController.list(); + if (!mounted || generation !== mountGeneration) return; + const sessionExists = authoritative?.sessions?.some(session => session.action === existingView.action); + if (sessionExists) { + setView(viewId); + return; + } + closeView(viewId, { skipEmbeddedClose: true }); + } catch (error) { + // If Main cannot be queried, preserve the existing renderer + // view rather than risk creating a duplicate native session. + console.warn(`[NextUI] Failed to reconcile embedded app ${app.id} before reopen:`, error); + setView(viewId); + return; + } } const host = ensureInternalHost(); const container = document.createElement('section'); @@ -742,7 +764,7 @@ accountMenuController = new AccountMenuController({ window, document, - getSettings: () => window.globalSettings || {}, + getSettings: () => chatCapabilities?.settings?.get?.() || {}, openSettings: () => window.uiHelperFunctions?.openModal?.('globalSettingsModal'), openAppearance: trigger => window.VCPAppearanceStudio?.open?.({ trigger }), openThemes: () => (window.chatAPI || window.electronAPI)?.openThemesWindow?.(), @@ -892,6 +914,24 @@ mount(); } + function provideChatCapabilities(capabilities) { + if (!capabilities || !capabilities.repository || typeof capabilities.getSnapshot !== 'function' + || typeof capabilities.createRenderer !== 'function' || !capabilities.manager) { + throw new TypeError('Next UI chat capabilities require repository, state snapshot, renderer factory and manager.'); + } + chatCapabilities = Object.freeze({ + repository: capabilities.repository, + getSnapshot: capabilities.getSnapshot, + createRenderer: capabilities.createRenderer, + manager: capabilities.manager, + presentation: capabilities.presentation || null, + settings: capabilities.settings || null, + }); + return () => { + if (chatCapabilities?.repository === capabilities.repository) chatCapabilities = null; + }; + } + // Internal applications may need to enter the shared VCPChat Agent/Group // creation flow. Expose the action itself rather than asking them to // synthesize a click on #nextUiCreateItemBtn: that element is part of a @@ -912,6 +952,7 @@ setView, acquireOverlay, releaseOverlay, + provideChatCapabilities, getDiagnostics: () => Object.freeze({ mounted, generation: mountGeneration, diff --git a/modules/ui-system/standalone-chat-app.js b/modules/ui-system/standalone-chat-app.js new file mode 100644 index 000000000..636fe541b --- /dev/null +++ b/modules/ui-system/standalone-chat-app.js @@ -0,0 +1,57 @@ +import { register } from './next-ui-apps.js'; +import { createReadOnlyChatSurface } from '../chat/chatSurface.js'; +import { createChatSurfaceSlots } from '../chat/chatSurfaceSlots.js'; +import { createChatPresentationState } from '../chat/chatPresentationState.js'; +import { createPresentationSkin } from '../chat/chatPresentationSkin.js'; +import { createChatThemePlugin } from '../chat/chatThemePlugin.js'; +import { createChatPluginLoader } from '../chat/chatPluginManifest.js'; + +function mountStandaloneChat(container, context = {}) { + const repository = context.chat?.repository; + const chatSnapshot = context.chat?.getSnapshot?.() || null; + const createRenderer = context.chat?.createRenderer; + const scope = context.scope?.child?.('next:standalone-chat') || null; + container.innerHTML = `

    聊天历史

    只读查看

    `; + const root = container.querySelector('.vcp-standalone-chat__messages'); + const focusButton = container.querySelector('.vcp-standalone-chat__focus'); + if (!repository || typeof createRenderer !== 'function') { + container.querySelector('.vcp-standalone-chat__status').textContent = '当前没有可查看的话题'; + return async () => { scope?.dispose?.('standalone-chat-empty'); container.replaceChildren(); }; + } + const rendererOwner = createRenderer({ root, mode: 'readonly', conversation: chatSnapshot }); + const renderer = rendererOwner.renderer; + const slots = createChatSurfaceSlots(); + const presentationState = createChatPresentationState({ ...(context.chat?.presentation?.getSnapshot?.() || {}), activeSurface: 'standalone' }); + const skin = createPresentationSkin({ id: 'readonly-badge', tokens: { accent: 'var(--vcp-accent-color)' }, render: (host, state, tokens) => { + host.dataset.presentationMode = state.mode; + host.dataset.skinAccent = tokens.accent; + return () => { delete host.dataset.presentationMode; delete host.dataset.skinAccent; }; + }, update: (host, state) => { host.dataset.presentationMode = state.mode; } }); + const themePlugin = createChatThemePlugin({ id: 'surface-default', tokens: { accent: 'var(--vcp-accent-color)', surface: 'var(--vcp-surface-color)' } }); + const pluginLoader = createChatPluginLoader({ state: presentationState, slots }); + const skinHost = container.querySelector('.vcp-standalone-chat > header'); + const uninstallBadge = pluginLoader.install({ + id: 'readonly-badge', apiVersion: 1, + capabilities: ['theme', 'surface-slot', 'presentation-state'], slots: ['header'] + }, ({ registerSlot, applyTheme, mountSkin }) => { + registerSlot('header', 'badge', (host) => { + host.textContent = '只读'; + host.setAttribute('aria-label', '只读聊天历史'); + }); + applyTheme(root, themePlugin); + mountSkin(skinHost, skin); + }); + const surface = createReadOnlyChatSurface({ root, renderer, repository, focusTarget: root, slots, presentationState, disposeRenderer: () => rendererOwner.dispose(), conversation: rendererOwner.conversation }); + surface.mountSlot('header', container.querySelector('.vcp-standalone-chat > header'), { canSend: false }); + const onFocus = () => surface.focus(); + focusButton.addEventListener('click', onFocus); + const load = chatSnapshot?.selectedItem?.id && chatSnapshot.topicId + ? surface.loadHistory(chatSnapshot.selectedItem.id, chatSnapshot.selectedItem.type, chatSnapshot.topicId, { initialBatch: 5, batchSize: 10, batchDelay: 80 }) + : Promise.resolve({ history: [], stale: false }); + load.then(result => { + if (!result.stale) container.querySelector('.vcp-standalone-chat__status').textContent = `${result.history.length} 条消息`; + }).catch(error => { container.querySelector('.vcp-standalone-chat__status').textContent = `加载失败:${error.message}`; }); + return async () => { focusButton.removeEventListener('click', onFocus); uninstallBadge(); await surface.dispose(); pluginLoader.dispose(); presentationState.dispose(); slots.dispose(); scope?.dispose?.('standalone-chat-unmounted'); container.replaceChildren(); }; +} + +register({ id: 'standalone-chat-history', title: '聊天历史', icon: 'chat', kind: 'internal', discoverable: false, mount: mountStandaloneChat }); diff --git a/modules/uiManager.js b/modules/uiManager.js index e2e40b8e5..c1bfaaafd 100644 --- a/modules/uiManager.js +++ b/modules/uiManager.js @@ -12,7 +12,32 @@ const uiManager = (() => { // DOM Elements (will be initialized in init) let leftSidebar, rightNotificationsSidebar, resizerLeft, resizerRight; let digitalClockElement, dateDisplayElement, notificationTitleElement; - let sidebarTabButtons, sidebarTabContents; + let sidebarTabButtons, sidebarTabContents; + let lifecycleOwner = null; + let settingsManagerCapability = null; + let itemListManagerCapability = null; + let uiHelperCapability = null; + let disposed = false; + let generation = 0; + let themeDisposer = null; + const tasks = new Set(); + const fallbackTimers = new Set(); + const isCurrent = token => !disposed && token === generation; + const track = value => { + const task = Promise.resolve(value); + tasks.add(task); + task.finally(() => tasks.delete(task)).catch(() => {}); + return task; + }; + const scheduleTimeout = (callback, delay) => { + if (lifecycleOwner?.timeout) return lifecycleOwner.timeout(callback, delay); + const timer = setTimeout(() => { + fallbackTimers.delete(timer); + if (!disposed) callback(); + }, delay); + fallbackTimers.add(timer); + return timer; + }; // --- Private Functions --- @@ -44,13 +69,14 @@ const uiManager = (() => { document.body.classList.toggle('vcp-sidebar-resizing', active); element.style.transition = active ? 'none' : ''; }, - onCommit: async (width) => { - const currentSettings = globalSettingsRef.get(); - const roundedWidth = Math.round(width); - if (currentSettings[settingKey] === roundedWidth) return; - currentSettings[settingKey] = roundedWidth; - try { - await electronAPI.saveSettings(currentSettings); + onCommit: async (width) => { + const currentSettings = globalSettingsRef.get(); + const roundedWidth = Math.round(width); + if (currentSettings[settingKey] === roundedWidth) return; + const nextSettings = { ...currentSettings, [settingKey]: roundedWidth }; + globalSettingsRef.set(nextSettings); + try { + await electronAPI.saveSettings(nextSettings); console.log('Sidebar width saved to settings.'); } catch (error) { console.error('Failed to save sidebar width:', error); @@ -72,7 +98,8 @@ const uiManager = (() => { * Applies the specified theme (light/dark) to the document body and updates the toggle button. * @param {string} theme - The theme to apply ('light' or 'dark'). */ - function applyTheme(theme) { + function applyTheme(theme) { + if (disposed) return false; if (!theme || (theme !== 'light' && theme !== 'dark')) { console.warn(`[UIManager] Invalid theme specified: ${theme}. Defaulting to system or light.`); // As a fallback, we'll default to light, but the initial theme should come from the main process. @@ -117,11 +144,13 @@ const uiManager = (() => { /** * Initializes theme handling by getting the current theme and listening for updates. */ - async function initializeTheme() { - // Listen for theme updates broadcast from the main process - if (electronAPI && electronAPI.onThemeUpdated) { - electronAPI.onThemeUpdated((theme) => { - const themeName = typeof theme === 'object' && theme !== null ? theme.theme : theme; + async function initializeTheme(token) { + // Listen for theme updates broadcast from the main process + if (electronAPI && electronAPI.onThemeUpdated) { + themeDisposer?.(); + themeDisposer = electronAPI.onThemeUpdated((theme) => { + if (!isCurrent(token)) return; + const themeName = typeof theme === 'object' && theme !== null ? theme.theme : theme; if (themeName) { applyTheme(themeName); } @@ -142,8 +171,9 @@ const uiManager = (() => { console.warn('[UIManager] currentThemeMode not found in settings, falling back to requesting from main process.'); if (electronAPI && electronAPI.getCurrentTheme) { try { - const currentTheme = await electronAPI.getCurrentTheme(); - applyTheme(currentTheme); + const currentTheme = await electronAPI.getCurrentTheme(); + if (!isCurrent(token)) return false; + applyTheme(currentTheme); } catch (error) { console.error('[UIManager] Fallback failed to get initial theme:', error); applyTheme('light'); // Final fallback @@ -180,12 +210,16 @@ const uiManager = (() => { /** * Initializes the digital clock display. */ - function initializeDigitalClock() { + function initializeDigitalClock() { if (digitalClockElement && notificationTitleElement && dateDisplayElement) { notificationTitleElement.style.display = 'none'; updateDateTimeDisplay(); // 每分钟更新一次时间显示,减少不必要的每秒刷新 - setInterval(updateDateTimeDisplay, 60000); + const scheduleClock = () => { + updateDateTimeDisplay(); + scheduleTimeout(scheduleClock, 60000); + }; + scheduleTimeout(scheduleClock, 60000); } else { console.error('Digital clock, notification title, or date display element not found.'); } @@ -253,9 +287,9 @@ const uiManager = (() => { e.stopPropagation(); // 打开全局设置模态框 - if (window.uiHelperFunctions && window.uiHelperFunctions.openModal) { - console.log('[UIManager] Middle click on settings tab - opening global settings modal'); - window.uiHelperFunctions.openModal('globalSettingsModal'); + if ((uiHelperCapability || window.uiHelperFunctions)?.openModal) { + console.log('[UIManager] Middle click on settings tab - opening global settings modal'); + (uiHelperCapability || window.uiHelperFunctions).openModal('globalSettingsModal'); } else { console.warn('[UIManager] uiHelperFunctions.openModal not available'); } @@ -294,7 +328,7 @@ const uiManager = (() => { const scheduleCompactMenuClose = () => { if (closeMenuTimer) clearTimeout(closeMenuTimer); - closeMenuTimer = setTimeout(() => setCompactMenuOpen(false), 200); + closeMenuTimer = scheduleTimeout(() => setCompactMenuOpen(false), 200); }; const closeTopicDrawer = () => { @@ -355,9 +389,9 @@ const uiManager = (() => { if (action === 'settings') { closeTopicDrawer(); leftSidebar.classList.remove('avatar-only'); - const settings = globalSettingsRef.get(); - settings.sidebarAvatarOnly = false; - electronAPI?.saveSettings?.(settings).catch(error => { + const settings = { ...globalSettingsRef.get(), sidebarAvatarOnly: false }; + globalSettingsRef.set(settings); + electronAPI?.saveSettings?.(settings).catch(error => { console.error('[UIManager] Failed to save compact sidebar state:', error); }); switchToTab('settings'); @@ -400,9 +434,9 @@ const uiManager = (() => { // “仅头像”是助手列表专属布局;进入话题或设置时恢复完整侧栏。 if (targetTab !== 'agents' && leftSidebar?.classList.contains('avatar-only')) { leftSidebar.classList.remove('avatar-only'); - const settings = globalSettingsRef.get(); - settings.sidebarAvatarOnly = false; - electronAPI?.saveSettings?.(settings).catch(error => { + const settings = { ...globalSettingsRef.get(), sidebarAvatarOnly: false }; + globalSettingsRef.set(settings); + electronAPI?.saveSettings?.(settings).catch(error => { console.error('[UIManager] Failed to save avatar-only sidebar state:', error); }); } @@ -429,26 +463,28 @@ const uiManager = (() => { // 刷新计数 refreshUnreadCounts(); } else if (targetTab === 'settings') { - if (window.settingsManager) { + if (settingsManagerCapability || window.settingsManager) { // 检查是否有待刷新的 Agent const pendingAgentId = sessionStorage.getItem('pendingAgentReload'); if (pendingAgentId) { console.log('[UIManager] Detected pending agent reload, reloading:', pendingAgentId); sessionStorage.removeItem('pendingAgentReload'); // 延迟执行以确保标签页切换完成 - setTimeout(() => { - if (window.settingsManager && typeof window.settingsManager.reloadAgentSettings === 'function') { - window.settingsManager.reloadAgentSettings(pendingAgentId); - } - }, 50); + const reload = () => { + if (disposed) return; + if ((settingsManagerCapability || window.settingsManager)?.reloadAgentSettings) { + track((settingsManagerCapability || window.settingsManager).reloadAgentSettings(pendingAgentId)); + } + }; + scheduleTimeout(reload, 50); } else { - window.settingsManager.displaySettingsForItem(); + (settingsManagerCapability || window.settingsManager)?.displaySettingsForItem?.(); } } } else if (targetTab === 'agents') { // Assuming 'agents' is the ID for the items list tab content // 重置鼠标事件状态,确保双击功能正常工作 - if (window.itemListManager && typeof window.itemListManager.resetMouseEventStates === 'function') { - window.itemListManager.resetMouseEventStates(); + if ((itemListManagerCapability || window.itemListManager)?.resetMouseEventStates) { + (itemListManagerCapability || window.itemListManager).resetMouseEventStates(); } // 刷新计数 refreshUnreadCounts(); @@ -464,12 +500,14 @@ const uiManager = (() => { /** * 刷新未读计数 */ - async function refreshUnreadCounts() { - try { - const result = await electronAPI.getUnreadTopicCounts(); + async function refreshUnreadCounts() { + const token = generation; + try { + const result = await electronAPI.getUnreadTopicCounts(); + if (!isCurrent(token)) return; if (result && result.success) { - if (window.itemListManager && typeof window.itemListManager.updateUnreadBadges === 'function') { - window.itemListManager.updateUnreadBadges(result.counts); + if ((itemListManagerCapability || window.itemListManager)?.updateUnreadBadges) { + (itemListManagerCapability || window.itemListManager).updateUnreadBadges(result.counts); } } } catch (error) { @@ -480,9 +518,15 @@ const uiManager = (() => { // --- Public API --- return { - init: async (options) => { - electronAPI = options.electronAPI; - globalSettingsRef = options.refs.globalSettingsRef; + init: async (options) => { + if (disposed) throw new Error('UIManager has been disposed'); + const token = ++generation; + electronAPI = options.electronAPI; + globalSettingsRef = options.refs.globalSettingsRef; + lifecycleOwner = options.listenerOwner || null; + settingsManagerCapability = options.settingsManager || null; + itemListManagerCapability = options.itemListManager || null; + uiHelperCapability = options.uiHelper || null; // Assign DOM elements from options.elements leftSidebar = options.elements.leftSidebar; @@ -496,18 +540,41 @@ const uiManager = (() => { sidebarTabContents = options.elements.sidebarTabContents; // Initialize all features - initializeResizers(); - await initializeTheme(); // Replaces loadAndApplyThemePreference - initializeDigitalClock(); - setupSidebarTabs(); - setupCompactSidebarNavigation(); + const releaseCapturedListeners = lifecycleOwner?.capture?.() || (() => {}); + try { + initializeResizers(); + await track(initializeTheme(token)); // Replaces loadAndApplyThemePreference + if (!isCurrent(token)) return false; + initializeDigitalClock(); + setupSidebarTabs(); + setupCompactSidebarNavigation(); + } finally { + releaseCapturedListeners(); + } console.log('uiManager initialized.'); }, applyTheme: applyTheme, // Expose applyTheme if needed externally getThemeState: () => themeChannel?.get() || Object.freeze({ ready: true, effective: document.body.classList.contains('dark-theme') ? 'dark' : 'light' }), subscribeTheme: (listener, options) => themeChannel?.subscribe(listener, options) || (() => false), - switchToTab: switchToTab // Expose switchToTab for external use + switchToTab: switchToTab // Expose switchToTab for external use + ,dispose: async () => { + if (disposed) return; + disposed = true; + generation += 1; + themeDisposer?.(); + themeDisposer = null; + for (const timer of fallbackTimers) clearTimeout(timer); + fallbackTimers.clear(); + await Promise.allSettled([...tasks]); + lifecycleOwner = null; + settingsManagerCapability = null; + itemListManagerCapability = null; + uiHelperCapability = null; + leftSidebar = rightNotificationsSidebar = resizerLeft = resizerRight = null; + digitalClockElement = dateDisplayElement = notificationTitleElement = null; + sidebarTabButtons = sidebarTabContents = null; + } }; })(); diff --git a/modules/weatherService.js b/modules/weatherService.js index 248cf70d4..2a049783c 100644 --- a/modules/weatherService.js +++ b/modules/weatherService.js @@ -17,6 +17,7 @@ const weatherService = (() => { let lastFetchTime = 0; const CACHE_DURATION = 30 * 60 * 1000; // 30分钟缓存 let refreshTimer = null; + let getGlobalSettings = () => ({}); /** * 从 vcpServerUrl 推导出天气 API 的 URL @@ -329,7 +330,7 @@ const weatherService = (() => { * 初始化天气服务 */ async function init() { - const globalSettings = window.globalSettings || {}; + const globalSettings = getGlobalSettings(); if (globalSettings.enableWeatherCard === false) { console.log('[WeatherService] Weather card disabled in settings'); return; @@ -346,7 +347,7 @@ const weatherService = (() => { // 设置定时刷新(每30分钟) if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(async () => { - const gs = window.globalSettings || {}; + const gs = getGlobalSettings(); if (gs.enableWeatherCard === false) return; const freshData = await getWeatherData(true); if (freshData) { @@ -385,6 +386,11 @@ const weatherService = (() => { } return { + configureCapabilities({ settings = null } = {}) { + getGlobalSettings = typeof settings?.get === 'function' + ? () => settings.get() || {} + : () => ({}); + }, init, destroy, toggle, diff --git a/package.json b/package.json index d9ae6ea21..10329da11 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,22 @@ "guard:classic-parity": "node scripts/check-classic-parity.mjs", "guard:classic-retirement": "node scripts/check-classic-retirement-boundary.mjs", "guard:next-delta": "node scripts/check-next-delta-contract.mjs", + "guard:chat-kernel-consumers": "node scripts/check-chat-kernel-consumers.mjs", + "build:chat-event-graph": "node scripts/build-chat-event-graph.mjs", + "check:chat-contracts": "node scripts/check-chat-contracts.mjs", + "test:chat-contract-invariants": "node scripts/run-chat-contract-invariants.mjs && node scripts/test-chat-contract-invalid.mjs", + "check:artifact-plane": "node scripts/check-artifact-plane.mjs", + "test:built-artifact-smoke": "node scripts/test-built-artifact-smoke.mjs", + "test:chat-transcript-snapshot": "node scripts/test-chat-transcript-snapshot.mjs", + "check:chat-evidence": "node scripts/check-chat-evidence.mjs", + "check:chat-release-evidence": "node scripts/check-chat-release-evidence.mjs", + "write:chat-evidence-manifest": "node scripts/write-chat-evidence-manifest.mjs", + "test:chat-evidence-manifest": "node scripts/test-chat-evidence-manifest.mjs", + "test:facade-registry-invalid": "node scripts/test-facade-registry-invalid.mjs", + "test:artifact-plane-invalid": "node scripts/test-artifact-plane-invalid.mjs", + "test:packaged-artifact-smoke": "node scripts/test-packaged-artifact-smoke.mjs", + "test:packaged-artifact-invalid": "node scripts/test-packaged-artifact-invalid.mjs", "guard:ui-system": "node scripts/check-ui-system.mjs && node scripts/check-vcpui-consumers.mjs", - "guard:ui-interaction": "node scripts/check-ui-interaction-inventory.mjs", - "guard:ui-async-state": "node scripts/check-ui-async-state-matrix.mjs", - "guard:ui-task-journeys": "node scripts/check-ui-task-journeys.mjs", - "guard:theme-provenance": "node scripts/check-theme-provenance.mjs", - "check:ui-harness-evidence": "node scripts/check-ui-harness-evidence.mjs", "check:ui-applications": "node scripts/check-ui-applications.mjs && node scripts/test-page-runtime.mjs", "pack:check": "npm run vendor:webawesome:check && node scripts/check-webawesome-pack.mjs", "vendor:webawesome:report": "node scripts/build-webawesome-runtime.mjs", @@ -51,6 +61,8 @@ "test:electron-ui-apps": "node scripts/test-electron-ui-apps-smoke.mjs", "test:electron-lifecycle-stress": "node scripts/test-electron-lifecycle-stress.mjs", "test:electron-main-chat-sequences": "node scripts/test-electron-main-chat-sequences.mjs", + "test:windows-matrix": "node scripts/test-electron-windows-matrix.mjs", + "test:manual-soak": "node scripts/test-electron-manual-soak.mjs", "test:electron-stability": "npm run test:electron-ui-apps && npm run test:electron-main-chat-sequences && npm run test:electron-lifecycle-stress", "pack": "npm run build && electron-builder --dir", "dist": "npm run build && electron-builder", diff --git a/renderer.js b/renderer.js index 89e3c6673..c5fedaf1e 100755 --- a/renderer.js +++ b/renderer.js @@ -1,5 +1,32 @@ +import { createChatHistoryPersistence } from './modules/chat/chatHistoryPersistence.js'; +import { createChatHistoryMutationAuthority } from './modules/chat/chatHistoryMutationAuthority.js'; +import { createSurfaceConversation } from './modules/chat/surfaceConversation.js'; +import { createStreamTransientHistory } from './modules/chat/streamTransientHistory.js'; +import { createMessageRenderer } from './modules/messageRenderer.js'; +import { applyUserMessageLayoutState } from './modules/renderer/domBuilder.js'; +import { createStreamProjection } from './modules/renderer/streamManager.js'; +import { createMainChatEventBridge } from './modules/renderer/mainChatEventBridge.js'; +import { createOwnedPreloadSubscription } from './modules/renderer/ownedPreloadSubscription.js'; +import { createTopicSelectionReadiness } from './modules/renderer/topicSelectionReadiness.js'; +import { createTtsSurfaceOwner } from './modules/renderer/ttsSurfaceOwner.js'; +import { createMainChatAuxiliaryEventOwner } from './modules/renderer/mainChatAuxiliaryEventOwner.js'; +import { createMainChatFlowlockOwner } from './modules/renderer/mainChatFlowlockOwner.js'; +import { createForwardMessageOwner } from './modules/renderer/forwardMessageOwner.js'; +import { createMainChatSettingsOwner } from './modules/renderer/mainChatSettingsOwner.js'; +import { createDomListenerOwner } from './modules/renderer/domListenerOwner.js'; +import { createMainChatThemeOwner } from './modules/renderer/mainChatThemeOwner.js'; +import { createMainChatSettingsPresentationOwner } from './modules/renderer/mainChatSettingsPresentationOwner.js'; +import { createMainChatAttachmentOwner } from './modules/renderer/mainChatAttachmentOwner.js'; +import { createMainChatSendOwner } from './modules/renderer/mainChatSendOwner.js'; + +const streamManager = createStreamProjection(); +const messageRenderer = createMessageRenderer({ streamManager }); +import { chatManager } from './modules/chatManager.js'; + +window.VCPLifecycleInspector?.setStreamDiagnosticsProvider?.(() => streamManager.getDiagnostics()); + // --- Globals --- -let globalSettings = { +const mainChatSettingsOwner = createMainChatSettingsOwner({ initial: { sidebarWidth: 260, showHomeVisualBrand: true, showHomeVisualTagline: true, @@ -59,241 +86,69 @@ let globalSettings = { providerUrl: 'https://api.siliconflow.cn', providerKey: '' } -}; -// Unified selected item state -let currentSelectedItem = { +} }); +const initialSelectedItem = { id: null, // Can be agentId or groupId type: null, // 'agent' or 'group' name: null, avatarUrl: null, config: null // Store full config object for the selected item }; -let currentTopicId = null; -let currentChatHistory = []; -window.__vcpRendererReady = false; -window.__vcpPendingTopicSelection = null; +const mainChatStateAuthority = createMainChatStateAuthority({ selectedItem: initialSelectedItem, topicId: null, history: [] }); +const currentSelectedItemRef = mainChatStateAuthority.selectedItemRef; +const currentTopicIdRef = mainChatStateAuthority.topicIdRef; +const mainHistoryRef = mainChatStateAuthority.historyRef; +const topicSelectionReadiness = createTopicSelectionReadiness(); const chatAPI = window.chatAPI || window.electronAPI; const STARTUP_SETTINGS_TIMEOUT_MS = 15_000; -// 暴露到window对象以便其他模块访问 -window.currentSelectedItem = currentSelectedItem; -window.currentTopicId = currentTopicId; -let attachedFiles = []; -let audioContext = null; -let currentAudioSource = null; -let ttsAudioQueue = []; // 新增:TTS音频播放队列 -let isTtsPlaying = false; // 新增:TTS播放状态标志 -let currentPlayingMsgId = null; // 新增:跟踪当前播放的msgId以控制UI -let currentTtsSessionId = -1; // 新增:会话ID,用于处理异步时序问题 - -// --- DOM Elements --- -const itemListUl = document.getElementById('agentList'); // Renamed from agentListUl to itemListUl -const currentChatNameH3 = document.getElementById('currentChatAgentName'); // Will show Agent or Group name -const chatMessagesDiv = document.getElementById('chatMessages'); -const messageInput = document.getElementById('messageInput'); -const sendMessageBtn = document.getElementById('sendMessageBtn'); -const attachFileBtn = document.getElementById('attachFileBtn'); -const emoticonTriggerBtn = document.getElementById('emoticonTriggerBtn'); -const quickNewTopicBtn = document.getElementById('quickNewTopicBtn'); -const attachmentPreviewArea = document.getElementById('attachmentPreviewArea'); -const chatInputCard = document.querySelector('.chat-input-card'); - -const globalSettingsBtn = document.getElementById('globalSettingsBtn'); +// Plugin-facing state is read-only; mutation authority remains here. +Object.defineProperty(window, 'VCPMainChatState', { + value: Object.freeze(mainChatStateAuthority.consumer), + writable: false, + configurable: false, +}); +const getGlobalSettings = () => mainChatSettingsOwner.get(); + +// --- Main-window composition bindings --- +const { + itemListUl, currentChatNameH3, chatMessagesDiv, messageInput, sendMessageBtn, + attachFileBtn, emoticonTriggerBtn, quickNewTopicBtn, attachmentPreviewArea, + chatInputCard, globalSettingsBtn, itemSettingsContainerTitle, + selectedItemNameForSettingsSpan, agentSettingsContainer, agentSettingsForm, + editingAgentIdInput, agentNameInput, agentAvatarInput, agentAvatarPreview, + agentSystemPromptTextarea, agentModelInput, agentTemperatureInput, + agentContextTokenLimitInput, agentMaxOutputTokensInput, groupSettingsContainer, + selectItemPromptForSettings, deleteItemBtn, currentItemActionBtn, + clearCurrentChatBtn, toggleNotificationsBtn, notificationsSidebar, + vcpLogConnectionStatusDiv, notificationsListUl, sidebarTabButtons, + sidebarTabContents, tabContentTopics, tabContentSettings, topicSearchInput, + leftSidebar, rightNotificationsSidebar, resizerLeft, resizerRight, + agentSearchInput, notificationTitleElement, digitalClockElement, + dateDisplayElement, toggleAssistantBtn, toggleSidebarModeBtn, openModelSelectBtn, +} = createMainChatDomBindings(document); // 模态框及其内部元素现在延迟加载,不再在顶层缓存引用 let globalSettingsForm = null; let userAvatarInput = null; let userAvatarPreview = null; - -const itemSettingsContainerTitle = document.getElementById('agentSettingsContainerTitle'); // Will be itemSettingsContainerTitle -const selectedItemNameForSettingsSpan = document.getElementById('selectedAgentNameForSettings'); // Will show Agent or Group name - -// Agent specific settings elements (will be hidden if a group is selected) -const agentSettingsContainer = document.getElementById('agentSettingsContainer'); -const agentSettingsForm = document.getElementById('agentSettingsForm'); -const editingAgentIdInput = document.getElementById('editingAgentId'); -const agentNameInput = document.getElementById('agentNameInput'); -const agentAvatarInput = document.getElementById('agentAvatarInput'); -const agentAvatarPreview = document.getElementById('agentAvatarPreview'); -const agentSystemPromptTextarea = document.getElementById('agentSystemPrompt'); -const agentModelInput = document.getElementById('agentModel'); -const agentTemperatureInput = document.getElementById('agentTemperature'); -const agentContextTokenLimitInput = document.getElementById('agentContextTokenLimit'); -const agentMaxOutputTokensInput = document.getElementById('agentMaxOutputTokens'); - -// Group specific settings elements (placeholder, grouprenderer.js will populate) -const groupSettingsContainer = document.getElementById('groupSettingsContainer'); // This should be the div renderer creates - -const selectItemPromptForSettings = document.getElementById('selectAgentPromptForSettings'); // Will be "Select an item..." console.log('[Renderer EARLY CHECK] selectItemPromptForSettings element:', selectItemPromptForSettings); // 添加日志 -const deleteItemBtn = document.getElementById('deleteAgentBtn'); // Will be deleteItemBtn for agent or group - -const currentItemActionBtn = document.getElementById('currentAgentSettingsBtn'); // Text will change (e.g. "New Topic" / "New Group Topic") -const clearCurrentChatBtn = document.getElementById('clearCurrentChatBtn'); -const toggleNotificationsBtn = document.getElementById('toggleNotificationsBtn'); - -const notificationsSidebar = document.getElementById('notificationsSidebar'); -const vcpLogConnectionStatusDiv = document.getElementById('vcpLogConnectionStatus'); -const notificationsListUl = document.getElementById('notificationsList'); - -const sidebarTabButtons = document.querySelectorAll('.sidebar-tab-button'); -const sidebarTabContents = document.querySelectorAll('.sidebar-tab-content'); -const tabContentTopics = document.getElementById('tabContentTopics'); -const tabContentSettings = document.getElementById('tabContentSettings'); - -const topicSearchInput = document.getElementById('topicSearchInput'); // Should be in tabContentTopics -const DEFAULT_SEND_BUTTON_HTML = sendMessageBtn?.innerHTML || ''; -const INTERRUPT_SEND_BUTTON_HTML = ` - -`; - function isContextForCurrentChat(context) { + const currentSelectedItem = currentSelectedItemRef.get(); + const currentTopicId = currentTopicIdRef.get(); if (!context || !currentSelectedItem?.id || !currentTopicId) return false; const contextItemId = context.groupId || context.agentId; return contextItemId === currentSelectedItem.id && context.topicId === currentTopicId; } -function getInterruptibleMessageForCurrentChat() { - if (Array.isArray(currentChatHistory) && currentChatHistory.length > 0) { - for (let i = currentChatHistory.length - 1; i >= 0; i--) { - const message = currentChatHistory[i]; - if (!message || message.role !== 'assistant') continue; - - const messageItem = chatMessagesDiv?.querySelector(`.message-item[data-message-id="${message.id}"]`); - const isStreaming = Boolean(messageItem?.classList.contains('streaming')); - if (message.isThinking === true || isStreaming) { - return { ...message, isStreaming }; - } - } - } - - const activeStreamingMessageId = window.streamManager?.getActiveStreamingMessageId?.(); - const activeStreamingContext = window.streamManager?.getActiveStreamingContext?.(); - if (!activeStreamingMessageId || !isContextForCurrentChat(activeStreamingContext)) { - return null; - } - - const activeStreamingMessage = currentChatHistory.find( - (message) => message?.id === activeStreamingMessageId && message.role === 'assistant' - ); - if (activeStreamingMessage) { - return { ...activeStreamingMessage, isStreaming: true }; - } - - return { - id: activeStreamingMessageId, - role: 'assistant', - name: activeStreamingContext.agentName || currentSelectedItem?.name || currentSelectedItem?.id, - agentId: activeStreamingContext.agentId, - groupId: activeStreamingContext.groupId, - isGroupMessage: activeStreamingContext.isGroupMessage === true, - avatarUrl: activeStreamingContext.avatarUrl || currentSelectedItem?.avatarUrl, - avatarColor: activeStreamingContext.avatarColor || currentSelectedItem?.config?.avatarCalculatedColor, - isStreaming: true - }; -} - -function updateSendButtonState() { - if (!sendMessageBtn) return; - - const nextMode = getInterruptibleMessageForCurrentChat() ? 'interrupt' : 'send'; - sendMessageBtn.dataset.mode = nextMode; - sendMessageBtn.classList.toggle('interrupt-mode', nextMode === 'interrupt'); - sendMessageBtn.innerHTML = nextMode === 'interrupt' ? INTERRUPT_SEND_BUTTON_HTML : DEFAULT_SEND_BUTTON_HTML; - sendMessageBtn.title = nextMode === 'interrupt' ? '中止回复' : '发送消息/右键高级回复'; - sendMessageBtn.setAttribute('aria-label', nextMode === 'interrupt' ? '中止回复' : '发送消息'); - sendMessageBtn.setAttribute('aria-busy', String(nextMode === 'interrupt')); -} - -async function interruptActiveResponseFromSendButton() { - const activeMessage = getInterruptibleMessageForCurrentChat(); - if (!activeMessage) return false; - - const isGroupMessage = activeMessage.isGroupMessage === true || currentSelectedItem?.type === 'group'; - const messageContext = { - agentId: activeMessage.agentId || (isGroupMessage ? null : currentSelectedItem?.id), - groupId: activeMessage.groupId || (isGroupMessage ? currentSelectedItem?.id : null), - topicId: currentTopicId, - isGroupMessage, - agentName: activeMessage.name || currentSelectedItem?.name || currentSelectedItem?.id, - avatarUrl: activeMessage.avatarUrl || currentSelectedItem?.avatarUrl, - avatarColor: activeMessage.avatarColor || currentSelectedItem?.config?.avatarCalculatedColor - }; - - let result = { success: false, error: '无法发送中止请求。' }; - if (isGroupMessage) { - if (chatAPI && typeof chatAPI.interruptGroupRequest === 'function') { - result = await chatAPI.interruptGroupRequest(activeMessage.id); - } else { - result = { success: false, error: '群聊中止接口不可用。' }; - } - } else if (interruptHandler && typeof interruptHandler.interrupt === 'function') { - result = await interruptHandler.interrupt(activeMessage.id); - } - - if (result.success) { - // 与消息右键菜单保持一致:中止成功后等待主进程发送最终的 end/error 事件。 - // 不在此处提前 finalize,否则仍在 IPC 队列中的最后几个 chunk 会因消息已 finalized - // 而被 appendStreamChunk 丢弃,导致已经输出到界面上的半截内容无法完整落盘。 - uiHelperFunctions?.showToastNotification?.('已发送中止信号。', 'success'); - return true; - } - - // 只有后端中止请求失败时才本地兜底收尾,避免消息永久停留在流式状态。 - if (window.messageRenderer && typeof window.messageRenderer.finalizeStreamedMessage === 'function') { - await window.messageRenderer.finalizeStreamedMessage( - activeMessage.id, - 'cancelled_by_user', - messageContext, - { error: result.error || '无法发送中止请求,已在本地停止流式消息。' } - ); - } - - updateSendButtonState(); - uiHelperFunctions?.showToastNotification?.(`中止失败:${result.error || '未知错误'},已在本地停止。`, 'error'); - return true; -} - -async function handleSendButtonAction() { - if (getInterruptibleMessageForCurrentChat()) { - await interruptActiveResponseFromSendButton(); - return; - } - - if (window.chatManager && typeof window.chatManager.handleSendMessage === 'function') { - await window.chatManager.handleSendMessage(); - } -} - -window.updateSendButtonState = updateSendButtonState; -window.handleSendButtonAction = handleSendButtonAction; -updateSendButtonState(); - -const leftSidebar = document.querySelector('.sidebar'); -const rightNotificationsSidebar = document.getElementById('notificationsSidebar'); -const resizerLeft = document.getElementById('resizerLeft'); -const resizerRight = document.getElementById('resizerRight'); - -const agentSearchInput = document.getElementById('agentSearchInput'); - // Cropped file state is now managed within modules/ui-helpers.js - -const notificationTitleElement = document.getElementById('notificationTitle'); -const digitalClockElement = document.getElementById('digitalClock'); -const dateDisplayElement = document.getElementById('dateDisplay'); let inviteAgentButtonsContainerElement; // 新增:邀请发言按钮容器的引用 // Assistant settings elements -const toggleAssistantBtn = document.getElementById('toggleAssistantBtn'); // New button -const toggleSidebarModeBtn = document.getElementById('toggleSidebarModeBtn'); // 模态框内部元素延迟加载 let assistantAgentContainer = null; let assistantAgentSelect = null; // Model selection elements -const openModelSelectBtn = document.getElementById('openModelSelectBtn'); let modelSelectModal = null; let modelList = null; let modelSearchInput = null; @@ -306,20 +161,238 @@ const uiHelperFunctions = window.uiHelperFunctions; import searchManager from './modules/searchManager.js'; -import { initialize as initializeEmoticonFixer } from './modules/renderer/emoticonUrlFixer.js'; import * as interruptHandler from './modules/interruptHandler.js'; import { StartupThemeGate, loadSettingsWithTimeout } from './modules/ui-system/startup-theme-gate.js'; import { setupEventListeners } from './modules/event-listeners.js'; +import { createChatContext } from './modules/chat/chatContext.js'; +import { createChatRepository } from './modules/chat/chatRepository.js'; +import { createMainChatComposition } from './modules/renderer/mainChatComposition.js'; +import { createMainChatDomBindings } from './modules/renderer/mainChatDomBindings.js'; +import { createMainChatStateAuthority } from './modules/chat/mainChatStateAuthority.js'; +import { createNonStreamingEventConsumer } from './modules/renderer/nonStreamingEventConsumer.js'; +import { createChatPresentationState } from './modules/chat/chatPresentationState.js'; + +const ttsSurfaceOwner = createTtsSurfaceOwner({ + subscribePlay: callback => chatAPI.onPlayTtsAudio(callback), + subscribeStop: callback => chatAPI.onStopTtsAudio(callback), + createAudioContext: () => new (window.AudioContext || window.webkitAudioContext)(), + decodeBase64: value => Uint8Array.from(atob(value), character => character.charCodeAt(0)).buffer, + updateSpeakingIndicator: (messageId, active) => uiHelperFunctions.updateSpeakingIndicator(messageId, active), + showError: message => uiHelperFunctions.showToastNotification(message, 'error'), +}); + +// First production seam for the Chat Kernel migration. Legacy refs remain +// compatible while consumers move to this explicit context. +const initialChatState = mainChatStateAuthority.snapshot(); +const chatContext = createChatContext({ + selectedItem: initialChatState.selectedItem, + topicId: initialChatState.topicId, + history: mainHistoryRef.get() +}); +const chatRepository = createChatRepository(chatAPI); +const historyMutationAuthority = createChatHistoryMutationAuthority({ repository: chatRepository }); +const presentationState = createChatPresentationState({ theme: document.body.classList.contains('dark-theme') ? 'dark' : 'light', activeSurface: 'main' }); +let mainChatDomRenderer = null; +let mainChatSurface = null; +let mainChatAdapter = null; +let nonStreamingEventConsumer = null; +let mainChatEventBridge = null; +const ownedRendererSubscriptions = new Set(); +const mainChatDomListenerOwner = createDomListenerOwner(); +const topicListDomListenerOwner = createDomListenerOwner(); +const mainChatAttachmentOwner = createMainChatAttachmentOwner({ + renderPreview: files => uiHelperFunctions.updateAttachmentPreview(files, attachmentPreviewArea), +}); +const mainChatSendOwner = createMainChatSendOwner({ + button: sendMessageBtn, + messagesRoot: chatMessagesDiv, + historyRef: mainHistoryRef, + selectedItemRef: currentSelectedItemRef, + topicIdRef: currentTopicIdRef, + streamProjection: streamManager, + chatAPI, + interruptHandler, + getAdapter: () => mainChatAdapter, + getChatManager: () => chatManager, + messageRenderer, + notify: (message, type) => uiHelperFunctions?.showToastNotification?.(message, type), +}); +const mainChatSettingsPresentationOwner = createMainChatSettingsPresentationOwner({ + documentRef: document, + settingsOwner: mainChatSettingsOwner, + listenerOwner: mainChatDomListenerOwner, + chatAPI, + elements: { + leftSidebar, + rightNotificationsSidebar, + vcpLogConnectionStatus: vcpLogConnectionStatusDiv, + toggleAssistant: toggleAssistantBtn, + toggleSidebarMode: toggleSidebarModeBtn, + }, + notificationRenderer: window.notificationRenderer, + messageRenderer, + windowRef: window, + getSettingsManager: () => window.settingsManager, + getAppearance: () => window.VCPAppearance, + getPretextBridge: () => window.pretextBridge, +}); +ownedRendererSubscriptions.add(mainChatSettingsPresentationOwner); +ownedRendererSubscriptions.add(mainChatDomListenerOwner); +ownedRendererSubscriptions.add(topicListDomListenerOwner); +ownedRendererSubscriptions.add(mainChatAttachmentOwner); +ownedRendererSubscriptions.add(mainChatSendOwner); +mainChatSendOwner.update(); +ownedRendererSubscriptions.add({ + async dispose() { + const receipt = { promise: Promise.resolve() }; + window.dispatchEvent(new CustomEvent('vcp-appearance-studio-dispose', { detail: receipt })); + await receipt.promise; + }, +}); +window.notificationRenderer?.configureCapabilities?.({ + filterManager: window.filterManager, + listenerOwner: mainChatDomListenerOwner, +}); +window.settingsManager?.configureCapabilities?.({ settings: mainChatSettingsOwner }); +window.weatherService?.configureCapabilities?.({ settings: mainChatSettingsOwner }); +window.dispatchEvent(new CustomEvent('vcp-appearance-studio-configure', { detail: { + settings: mainChatSettingsOwner, + appearance: window.VCPAppearance, + uiManager: window.uiManager, + presentation: Object.freeze({ normalize: normalizeChatPresentationMode, apply: applyChatPresentationMode }), +} })); +const mainChatThemeOwner = createMainChatThemeOwner({ + settingsOwner: mainChatSettingsOwner, + documentRef: document, + presentationState, + getUiManager: () => window.uiManager, + matchMedia: query => window.matchMedia(query), + saveSettings: settings => chatAPI.saveSettings(settings), + pretextBridge: window.pretextBridge, + refreshLayout: () => messageRenderer?.refreshLayoutDependentState?.(), + syncControls: mainChatSettingsPresentationOwner.syncPresentationControls, + captureAnchor: mainChatSettingsPresentationOwner.capturePresentationAnchor, + restoreAnchor: mainChatSettingsPresentationOwner.restorePresentationAnchor, + scheduleFrame: callback => requestAnimationFrame(callback), + notify: (message, type) => uiHelperFunctions?.showToastNotification?.(message, type), +}); +mainChatSettingsPresentationOwner.configureThemeOwner(mainChatThemeOwner); +ownedRendererSubscriptions.add(mainChatThemeOwner); +let releaseNextUiChatCapabilities = null; +const forwardMessageOwner = createForwardMessageOwner({ + chatAPI, + chatManager, + uiHelperFunctions, + getConversation: () => ({ item: currentSelectedItemRef.get(), topicId: currentTopicIdRef.get() }), +}); +ownedRendererSubscriptions.add(forwardMessageOwner); +const showForwardModal = message => forwardMessageOwner.show(message); + +function createOwnedInternalChatRenderer({ root, mode = 'readonly', handleSendMessage = null, conversation = null } = {}) { + if (!root?.querySelector) throw new TypeError('Internal chat renderer requires a Surface root'); + const conversationCapability = createSurfaceConversation({ + selectedItem: conversation?.selectedItem || currentSelectedItemRef.get(), + topicId: conversation?.topicId ?? currentTopicIdRef.get(), + }); + let localSettings = getGlobalSettings(); + let disposed = false; + const streamProjection = createStreamProjection(); + const transientStreamHistory = createStreamTransientHistory({ + repository: chatRepository, + currentHistory: { + get: () => conversationCapability.historyRef.get(), + replace: history => conversationCapability.historyRef.set(history), + }, + }); + const surfacePretextPrefix = `surface-${conversationCapability.id || Date.now().toString(36)}`; + const createSurfacePretextBridge = (bridge) => { + if (!bridge) return null; + const key = id => `${surfacePretextPrefix}:${id}`; + return Object.freeze({ + isReady: () => bridge.isReady?.() === true, + estimateHeight: (id, ...args) => bridge.estimateHeight?.(key(id), ...args), + evict: id => bridge.evict?.(key(id)), + }); + }; + const surfacePretextBridge = createSurfacePretextBridge(window.pretextBridge); + const renderer = createMessageRenderer({ + streamManager: streamProjection, + initializeStreamProjection: mode === 'interactive', + enableContextMenu: false, + enableMiddleClick: false, + exposeGlobalCommands: false, + }); + const surfaceFeedback = Object.freeze({ + resolveAttachmentFileVisual: uiHelperFunctions.resolveAttachmentFileVisual, + getCompiledRegex: uiHelperFunctions.getCompiledRegex, + regexFromString: uiHelperFunctions.regexFromString, + showToastNotification: uiHelperFunctions.showToastNotification, + scrollToBottom() { + const scrollRoot = root.closest('.chat-messages-container') || root; + scrollRoot.scrollTop = scrollRoot.scrollHeight; + }, + }); + renderer.initializeMessageRenderer({ + chatRepository, + historyMutationAuthority, + currentChatHistoryRef: conversationCapability.historyRef, + currentSelectedItemRef: conversationCapability.selectedItemRef, + currentTopicIdRef: conversationCapability.topicIdRef, + transientStreamHistory, + viewAuthority: { + isCurrent: context => { + const selected = conversationCapability.selectedItemRef.get(); + const topicId = conversationCapability.topicIdRef.get(); + const itemId = context?.groupId || context?.agentId; + return Boolean(selected?.id && topicId && itemId === selected.id && context?.topicId === topicId); + } + }, + globalSettingsRef: { get: () => localSettings, set: value => { localSettings = value; } }, + chatMessagesDiv: root, + electronAPI: chatAPI, + markedInstance, + morphdom: window.morphdom, + pretextBridge: surfacePretextBridge, + flowlockProtocol: window.flowlockProtocol, + uiHelper: surfaceFeedback, + interruptHandler: null, + messageCommands: { handleSendMessage }, + summarizeTopicFromMessages: async () => '', + handleCreateBranch: async () => false, + }); + return Object.freeze({ + renderer, + mode, + conversation: conversationCapability, + async dispose() { + if (disposed) return; + disposed = true; + await streamProjection.dispose(); + await renderer.disposeRootResources(root); + renderer.disposeRendererResources(); + conversationCapability.dispose(); + localSettings = null; + }, + }); +} const startupThemeGate = new StartupThemeGate({ document, applyTheme: applyInitialThemeClass, statusElement: document.getElementById('startupInitializationStatus'), }); +mainChatSettingsPresentationOwner.configureStartup({ + loadSettings: loadSettingsWithTimeout, + startupThemeGate, +}); // --- Initialization --- - document.addEventListener('DOMContentLoaded', async () => { + mainChatDomListenerOwner.add(document, 'DOMContentLoaded', async () => { + window.notificationRenderer?.configureCapabilities?.({ + filterManager: window.filterManager, + listenerOwner: mainChatDomListenerOwner, + }); // Initialize Emoticon Manager if (window.emoticonManager) { window.emoticonManager.initialize({ @@ -355,13 +428,13 @@ const startupThemeGate = new StartupThemeGate({ }, electronAPI: chatAPI, refs: { - currentSelectedItemRef: { get: () => currentSelectedItem }, + currentSelectedItemRef, }, mainRendererFunctions: { selectItem: (itemId, itemType, itemName, itemAvatarUrl, itemFullConfig) => { // Delayed binding - chatManager will be available when this is called - if (window.chatManager) { - return window.chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig); + if (chatManager) { + return chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig); } else { console.error('[ItemListManager] chatManager not available for selectItem'); } @@ -394,29 +467,17 @@ const startupThemeGate = new StartupThemeGate({ window.GroupRenderer.init({ electronAPI: chatAPI, - globalSettingsRef: { get: () => globalSettings, set: (newSettings) => globalSettings = newSettings }, - currentSelectedItemRef: { - get: () => currentSelectedItem, - set: (val) => { - currentSelectedItem = val; - window.currentSelectedItem = val; - } - }, - currentTopicIdRef: { - get: () => currentTopicId, - set: (val) => { - currentTopicId = val; - window.currentTopicId = val; - } - }, - messageRenderer: window.messageRenderer, // Will be initialized later, pass ref + globalSettingsRef: mainChatSettingsOwner.ref, + currentSelectedItemRef, + currentTopicIdRef, + messageRenderer, // Explicit provider; initialized below uiHelper: uiHelperFunctions, mainRendererElements: mainRendererElementsForGroupRenderer, // 使用构造好的对象 mainRendererFunctions: { // Pass shared functions with delayed binding loadItems: () => window.itemListManager ? window.itemListManager.loadItems() : console.error('[GroupRenderer] itemListManager not available'), selectItem: (itemId, itemType, itemName, itemAvatarUrl, itemFullConfig) => { - if (window.chatManager) { - return window.chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig); + if (chatManager) { + return chatManager.selectItem(itemId, itemType, itemName, itemAvatarUrl, itemFullConfig); } else { console.error('[GroupRenderer] chatManager not available for selectItem'); } @@ -424,15 +485,15 @@ const startupThemeGate = new StartupThemeGate({ highlightActiveItem: (itemId, itemType) => window.itemListManager ? window.itemListManager.highlightActiveItem(itemId, itemType) : console.error('[GroupRenderer] itemListManager not available'), displaySettingsForItem: () => window.settingsManager ? window.settingsManager.displaySettingsForItem() : console.error('[GroupRenderer] settingsManager not available'), loadTopicList: () => window.topicListManager ? window.topicListManager.loadTopicList() : console.error('[GroupRenderer] topicListManager not available'), - getAttachedFiles: () => attachedFiles, - clearAttachedFiles: () => { attachedFiles.length = 0; }, - updateAttachmentPreview: () => uiHelperFunctions.updateAttachmentPreview(attachedFiles, attachmentPreviewArea), + getAttachedFiles: mainChatAttachmentOwner.get, + clearAttachedFiles: mainChatAttachmentOwner.clear, + updateAttachmentPreview: mainChatAttachmentOwner.syncPreview, setCroppedFile: uiHelperFunctions.setCroppedFile, getCroppedFile: uiHelperFunctions.getCroppedFile, - setCurrentChatHistory: (history) => currentChatHistory = history, + setCurrentChatHistory: history => mainHistoryRef.set(history), displayTopicTimestampBubble: (itemId, itemType, topicId) => { - if (window.chatManager) { - return window.chatManager.displayTopicTimestampBubble(itemId, itemType, topicId); + if (chatManager) { + return chatManager.displayTopicTimestampBubble(itemId, itemType, topicId); } else { console.error('[GroupRenderer] chatManager not available for displayTopicTimestampBubble'); } @@ -448,53 +509,99 @@ const startupThemeGate = new StartupThemeGate({ } // Initialize other modules after GroupRenderer, in case they depend on its setup - if (window.messageRenderer) { + if (messageRenderer) { interruptHandler.initialize(chatAPI); - window.messageRenderer.initializeMessageRenderer({ - currentChatHistoryRef: { get: () => currentChatHistory, set: (val) => currentChatHistory = val }, - currentSelectedItemRef: { - get: () => currentSelectedItem, - set: (val) => { - currentSelectedItem = val; - window.currentSelectedItem = val; - } - }, - currentTopicIdRef: { - get: () => currentTopicId, - set: (val) => { - currentTopicId = val; - window.currentTopicId = val; - } - }, - globalSettingsRef: { get: () => globalSettings, set: (newSettings) => globalSettings = newSettings }, - chatMessagesDiv: chatMessagesDiv, + const historyPersistence = createChatHistoryPersistence(chatRepository); + const transientStreamHistory = createStreamTransientHistory({ + repository: chatRepository, + currentHistory: { get: mainHistoryRef.get, replace: mainHistoryRef.set }, + }); + const renderDependencies = { + chatRepository, + historyMutationAuthority, + currentChatHistoryRef: mainHistoryRef, + transientStreamHistory, + currentSelectedItemRef, + currentTopicIdRef, + globalSettingsRef: mainChatSettingsOwner.ref, + chatMessagesDiv, electronAPI: chatAPI, - markedInstance: markedInstance, // Assuming marked.js is loaded + markedInstance, + morphdom: window.morphdom, + pretextBridge: window.pretextBridge, + flowlockProtocol: window.flowlockProtocol, uiHelper: uiHelperFunctions, - interruptHandler: interruptHandler, // Pass the handler + showForwardModal, + ensureAudioContext: ttsSurfaceOwner.ensureAudioContext, + interruptHandler, + messageCommands: { + processFilesData: (...args) => chatManager.processFilesData(...args), + addAttachmentsToMessage: (...args) => chatManager.addAttachmentsToMessage(...args), + removeAttachmentFromMessage: (...args) => chatManager.removeAttachmentFromMessage(...args), + syncNextUiEmptyStateWithMessages: (...args) => chatManager.syncNextUiEmptyStateWithMessages(...args), + handleSendMessage: (...args) => chatManager.handleSendMessage(...args), + updateSendButtonState: mainChatSendOwner.update, + }, summarizeTopicFromMessages: (messages, agentName) => { - // Directly use the function from the summarizer module, which should be on the window scope - if (typeof window.summarizeTopicFromMessages === 'function') { - return window.summarizeTopicFromMessages(messages, agentName); - } else { - console.error('[MessageRenderer] summarizeTopicFromMessages function not found on window scope.'); - return `关于 "${messages.find(m=>m.role==='user')?.content.substring(0,15) || '...'}" (备用)`; - } + if (typeof window.summarizeTopicFromMessages === 'function') return window.summarizeTopicFromMessages(messages, agentName); + console.error('[MessageRenderer] summarizeTopicFromMessages function not found on window scope.'); + return `关于 "${messages.find(m => m.role === 'user')?.content.substring(0, 15) || '...'}" (备用)`; }, - handleCreateBranch: (selectedMessage) => { - if (window.chatManager) { - return window.chatManager.handleCreateBranch(selectedMessage); - } else { - console.error('[MessageRenderer] chatManager not available for handleCreateBranch'); + handleCreateBranch: selectedMessage => chatManager?.handleCreateBranch(selectedMessage), + }; + const mainComposition = createMainChatComposition({ + root: chatMessagesDiv, + messageInput, + messageRenderer, + streamProjection: streamManager, + chatRepository, + historyPersistence, + presentationState, + renderDependencies, + chatManager, + flowlockManager: window.flowlockManager, + currentSelection: currentSelectedItemRef.get, + currentTopicId: currentTopicIdRef.get, + chatWindow: window, + interrupt: mainChatSendOwner.interrupt, + dispatchTerminal: detail => window.dispatchEvent(new CustomEvent('vcp-chat-stream-terminal', { detail })), + notifySendStateChanged: mainChatSendOwner.update, + showForwardModal, + provideCapabilities: window.VCPNextShellController?.provideChatCapabilities, + capabilitySnapshot: () => Object.freeze({ + selectedItem: currentSelectedItemRef.get(), + topicId: currentTopicIdRef.get(), + }), + settings: mainChatSettingsOwner, + createInternalRenderer: createOwnedInternalChatRenderer, + disposeCapabilities: async () => { + await mainChatEventBridge?.dispose?.(); + mainChatEventBridge = null; + await nonStreamingEventConsumer?.dispose?.(); + nonStreamingEventConsumer = null; + releaseNextUiChatCapabilities?.(); + releaseNextUiChatCapabilities = null; + const subscriptions = [...ownedRendererSubscriptions].reverse(); + ownedRendererSubscriptions.clear(); + for (const subscription of subscriptions) { + try { + await subscription.dispose(); + } catch (error) { + console.error('[Renderer] Owned capability disposal failed:', error); + } } - } + }, }); - - // Pass the new function to the context menu - window.messageRenderer.setContextMenuDependencies({ - showForwardModal: showForwardModal, + mainChatAdapter = mainComposition.adapter; + mainChatSurface = mainChatAdapter.surface; + mainChatDomRenderer = mainChatAdapter.domRenderer; + nonStreamingEventConsumer = createNonStreamingEventConsumer({ + renderTarget: mainChatDomRenderer, + messageRenderer, + viewAuthority: { isCurrent: isContextForCurrentChat }, }); + releaseNextUiChatCapabilities = mainComposition.releaseCapabilities; } else { console.error('[RENDERER_INIT] messageRenderer module not found!'); @@ -505,18 +612,26 @@ const startupThemeGate = new StartupThemeGate({ messageInput: messageInput, dropTargetElement: chatInputCard, electronAPI: chatAPI, - attachedFiles: { get: () => attachedFiles, set: (val) => attachedFiles = val }, - updateAttachmentPreview: () => uiHelperFunctions.updateAttachmentPreview(attachedFiles, attachmentPreviewArea), - getCurrentAgentId: () => currentSelectedItem.id, // Corrected: pass a function that returns the ID - getCurrentTopicId: () => currentTopicId, + attachedFiles: mainChatAttachmentOwner.ref, + updateAttachmentPreview: mainChatAttachmentOwner.syncPreview, + getCurrentAgentId: () => currentSelectedItemRef.get()?.id, + getCurrentTopicId: currentTopicIdRef.get, uiHelper: uiHelperFunctions, + listenerOwner: mainChatDomListenerOwner, }); + ownedRendererSubscriptions.add({ dispose: () => window.inputEnhancer.dispose?.() }); } else { console.error('[RENDERER_INIT] inputEnhancer module not found!'); } - if (chatAPI?.onLoomShareTextToInput) { - chatAPI.onLoomShareTextToInput((sharedText) => { + const auxiliaryEventOwner = createMainChatAuxiliaryEventOwner({ + subscriptions: { + loomShareText: chatAPI?.onLoomShareTextToInput, + logStatus: chatAPI?.onVCPLogStatus, + logMessage: chatAPI?.onVCPLogMessage, + groupTopicUpdated: chatAPI?.onVCPGroupTopicUpdated, + }, + insertSharedText: (sharedText) => { if (!messageInput || typeof sharedText !== 'string' || !sharedText) return; const start = Number.isInteger(messageInput.selectionStart) ? messageInput.selectionStart @@ -530,16 +645,10 @@ const startupThemeGate = new StartupThemeGate({ messageInput.dispatchEvent(new Event('input', { bubbles: true })); messageInput.focus(); uiHelperFunctions?.showToastNotification?.('Loom 页面文本已加入输入框。', 'success'); - }); - } - - chatAPI.onVCPLogStatus((statusUpdate) => { - if (window.notificationRenderer) { - window.notificationRenderer.updateVCPLogStatus(statusUpdate, vcpLogConnectionStatusDiv); - } - }); - chatAPI.onVCPLogMessage((logData) => { - if (window.notificationRenderer) { + }, + consumeLogStatus: statusUpdate => window.notificationRenderer?.updateVCPLogStatus(statusUpdate, vcpLogConnectionStatusDiv), + consumeLogMessage: logData => { + if (!window.notificationRenderer) return; const computedStyle = getComputedStyle(document.body); const themeColors = { notificationBg: computedStyle.getPropertyValue('--notification-bg').trim(), @@ -551,302 +660,76 @@ const startupThemeGate = new StartupThemeGate({ }; // 修复:只传递一个 logData 参数,第二个参数显式传递 null,以匹配 preload 定义 window.notificationRenderer.renderVCPLogNotification(logData, null, notificationsListUl, themeColors); - } - }); - - // Unified listener for all VCP stream events (agent and group) - chatAPI.onVCPStreamEvent(async (eventData) => { - if (!window.messageRenderer) { - console.error("onVCPStreamEvent: messageRenderer not available."); - return; - } - - const { type, messageId, context, chunk, error, finish_reason, fullResponse } = eventData; - - if (!messageId) { - console.error("onVCPStreamEvent: Received event without a messageId. Cannot process.", eventData); - return; - } - - // --- Asynchronous Logic: Update data model regardless of UI state --- - // This is where you would update a global or context-specific data store - // For now, we pass the context to the messageRenderer which handles the history array. - - // --- UI Logic: Only render if the message's context matches the current view --- - // Directly use the global variables `currentSelectedItem` and `currentTopicId` from the renderer's scope. - // The `...Ref` objects are not defined in this scope. - const isRelevantToCurrentView = context && - currentSelectedItem && // Ensure currentSelectedItem is not null - (context.groupId ? context.groupId === currentSelectedItem.id : context.agentId === currentSelectedItem.id) && - context.topicId === currentTopicId; - - // console.log(`[onVCPStreamEvent] Received event type '${type}' for msg ${messageId}. Relevant to current view: ${isRelevantToCurrentView}`, context); - - // Data model updates should ALWAYS happen, regardless of the current view. - // UI updates (creating new DOM elements) should only happen if the view is relevant. - switch (type) { - case 'data': - window.messageRenderer.appendStreamChunk(messageId, chunk, context); - break; - - case 'end': { - const finalizedMessage = await window.messageRenderer.finalizeStreamedMessage( - messageId, - finish_reason || 'completed', - context, - { fullResponse, error } - ); - - const finalizedContext = finalizedMessage?.context || context; - const finalizedContent = finalizedMessage?.content || fullResponse || ''; - - if (finalizedContext && !finalizedContext.isGroupMessage && isRelevantToCurrentView) { - await window.chatManager.attemptTopicSummarizationIfNeeded(); - } - - // Flowlock 以消息身份为依据运行,与当前可见 Agent/Topic 完全解耦。 - // 使用 streamManager 实际落盘的完整文本,避免 end 事件未携带 fullResponse 时漏掉控制标记。 - await window.flowlockManager?.handleFinalizedMessage({ - type: 'end', - messageId, - context: finalizedContext, - content: finalizedContent, - finishReason: finalizedMessage?.finishReason || finish_reason || 'completed' - }); - break; - } - - case 'error': - console.error('VCP Stream Error on ID', messageId, ':', error, 'Context:', context); - - // --- Recovery Logic: Use accumulated text from main if fullResponse is missing --- - let finalContent = fullResponse || eventData.accumulatedResponse || ""; - if (finalContent && finalContent.trim() !== "") { - // Add a visual indicator that the stream was cut short - finalContent += "\n\n> [!WARNING]\n> **流式响应中断**: " + (error || "未知连接错误") + "。已保存已接收的部分内容。"; - } - - { - const finalizedMessage = await window.messageRenderer.finalizeStreamedMessage( - messageId, - 'error', - context, - { fullResponse: finalContent, error } - ); - - await window.flowlockManager?.handleFinalizedMessage({ - type: 'error', - messageId, - context: finalizedMessage?.context || context, - content: finalizedMessage?.content || finalContent, - finishReason: 'error', - error - }); - } - - if (isRelevantToCurrentView) { - const errorMsgItem = document.querySelector(`.message-item[data-message-id="${messageId}"] .md-content`); - if (errorMsgItem) { - errorMsgItem.innerHTML += `

    流错误: ${error}

    `; - } else { - window.messageRenderer.renderMessage({ - role: 'system', - content: `流处理错误 (ID: ${messageId}): ${error}`, - timestamp: Date.now(), - id: `err_${messageId}` - }); - } - } - break; - - // These events create new message bubbles, so they should only execute if the view is relevant. - case 'agent_thinking': - // Use startStreamingMessage for both visible and non-visible chats to ensure proper initialization - console.log(`[Renderer onVCPStreamEvent AGENT_THINKING] Initializing streaming for ${context.agentName} (msgId: ${messageId})`); - // 直接调用 streamManager 的 startStreamingMessage,它会处理所有初始化 - if (window.streamManager && typeof window.streamManager.startStreamingMessage === 'function') { - window.streamManager.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: context.agentName, - agentId: context.agentId, - avatarUrl: context.avatarUrl, - avatarColor: context.avatarColor, - content: '思考中...', - timestamp: Date.now(), - isThinking: true, - isGroupMessage: context.isGroupMessage || false, - groupId: context.groupId, - topicId: context.topicId, - context: context // Pass the full context - }); - } else if (window.messageRenderer && typeof window.messageRenderer.startStreamingMessage === 'function') { - // Fallback to messageRenderer if streamManager not available - window.messageRenderer.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: context.agentName, - agentId: context.agentId, - avatarUrl: context.avatarUrl, - avatarColor: context.avatarColor, - content: '思考中...', - timestamp: Date.now(), - isThinking: true, - isGroupMessage: context.isGroupMessage || false, - groupId: context.groupId, - topicId: context.topicId, - context: context - }); - } - break; - - case 'start': - // START事件时,思考消息应该已经存在了 - // 我们只需要确保消息已经初始化,如果没有则初始化 - console.log(`[Renderer onVCPStreamEvent START] Processing start event for ${context.agentName} (msgId: ${messageId})`); - - // 确保消息被初始化(如果agent_thinking被跳过) - if (window.streamManager && typeof window.streamManager.startStreamingMessage === 'function') { - // streamManager 会检查消息是否已存在,避免重复初始化 - window.streamManager.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: context.agentName, - agentId: context.agentId, - avatarUrl: context.avatarUrl, - avatarColor: context.avatarColor, - content: '', - timestamp: Date.now(), - isThinking: false, - isGroupMessage: context.isGroupMessage || false, - groupId: context.groupId, - topicId: context.topicId, - context: context - }); - } else if (window.messageRenderer && typeof window.messageRenderer.startStreamingMessage === 'function') { - window.messageRenderer.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: context.agentName, - agentId: context.agentId, - avatarUrl: context.avatarUrl, - avatarColor: context.avatarColor, - content: '', - timestamp: Date.now(), - isThinking: false, - isGroupMessage: context.isGroupMessage || false, - groupId: context.groupId, - topicId: context.topicId, - context: context - }); - } - - if (isRelevantToCurrentView) { - console.log(`[Renderer onVCPStreamEvent START] UI updated for visible chat ${context.agentName} (msgId: ${messageId})`); - } else { - console.log(`[Renderer onVCPStreamEvent START] History updated for non-visible chat ${context.agentName} (msgId: ${messageId})`); - } - break; - - case 'full_response': - // This also needs to update history unconditionally and render only if relevant. - // `renderFullMessage` should handle this logic. - if (isRelevantToCurrentView) { - console.log(`[Renderer onVCPStreamEvent FULL_RESPONSE] Rendering for ${context.agentName} (msgId: ${messageId})`); - window.messageRenderer.renderFullMessage(messageId, fullResponse, context.agentName, context.agentId); + }, + consumeGroupTopicUpdate: async ({ groupId, topicId, newTitle, topics }, lifecycle) => { + const selectedItem = currentSelectedItemRef.get(); + if (selectedItem?.id !== groupId || selectedItem.type !== 'group') return; + const config = selectedItem.config || selectedItem; + if (config) { + const nextConfig = { ...config }; + const topicIndex = Array.isArray(config.topics) + ? config.topics.findIndex(topic => topic.id === topicId) + : -1; + if (topicIndex >= 0) { + nextConfig.topics = config.topics.map((topic, index) => ( + index === topicIndex ? { ...topic, name: newTitle } : topic + )); } else { - // If not relevant, we need a way to update the history without rendering. - // Let's assume `renderFullMessage` needs a flag or we need a new function. - // For now, let's add a placeholder to history. - console.log(`[Renderer onVCPStreamEvent FULL_RESPONSE] History update for non-visible chat needed for msgId: ${messageId}`); - // This part is tricky. The message might not exist in history yet. - // Let's ensure `renderFullMessage` can handle this. - window.messageRenderer.renderFullMessage(messageId, fullResponse, context.agentName, context.agentId); - } - break; - - case 'no_ai_response': - console.log(`[onVCPStreamEvent] No AI response needed for messageId: ${messageId}. Message: ${eventData.message}`); - break; - - case 'remove_message': - if (isRelevantToCurrentView) { - console.log(`[onVCPStreamEvent] Removing message ${messageId} from UI.`); - window.messageRenderer.removeMessageById(messageId, false); // false: don't save history again + nextConfig.topics = topics; } - break; - - default: - console.warn(`[onVCPStreamEvent] Received unhandled event type: '${type}'`, eventData); - } - }); - - // Listener for group topic title updates - chatAPI.onVCPGroupTopicUpdated(async (eventData) => { - const { groupId, topicId, newTitle, topics } = eventData; - console.log(`[Renderer] Received topic update for group ${groupId}, topic ${topicId}: "${newTitle}"`); - if (currentSelectedItem.id === groupId && currentSelectedItem.type === 'group') { - // Update the currentSelectedItem's config if it's the active group - const config = currentSelectedItem.config || currentSelectedItem; - if (config && config.topics) { - const topicIndex = config.topics.findIndex(t => t.id === topicId); - if (topicIndex !== -1) { - config.topics[topicIndex].name = newTitle; - } else { // Topic might be new or ID changed, replace topics array - config.topics = topics; - } - } else if (config) { - config.topics = topics; + currentSelectedItemRef.set(selectedItem.config + ? { ...selectedItem, config: nextConfig } + : { ...selectedItem, ...nextConfig }); } - - - // If the topics tab is active, reload the list if (document.getElementById('tabContentTopics').classList.contains('active')) { await window.topicListManager.loadTopicList(); + if (!lifecycle.isActive()) return; } - // Removed toast notification as per user feedback - // if (uiHelperFunctions && uiHelperFunctions.showToastNotification) { - // uiHelperFunctions.showToastNotification(`群组 "${currentSelectedItem.name}" 的话题 "${newTitle}" 已自动总结并更新。`); - // } - console.log(`群组 "${currentSelectedItem.name}" 的话题 "${newTitle}" 已自动总结并更新 (通知已移除).`); - } + }, }); + auxiliaryEventOwner.mount(); + ownedRendererSubscriptions.add(auxiliaryEventOwner); + mainChatEventBridge = createMainChatEventBridge({ + chatAPI, + acceptStreamEvent: eventData => mainChatAdapter?.acceptStreamEvent(eventData) === true, + consumeNonStreamingEvent: eventData => nonStreamingEventConsumer?.consume(eventData), + }); // Initialize TopicListManager if (window.topicListManager) { window.topicListManager.init({ elements: { - topicListContainer: tabContentTopics, + topicListContainer: tabContentTopics, }, electronAPI: chatAPI, + chatRepository: chatRepository, refs: { - currentSelectedItemRef: { - get: () => currentSelectedItem - }, - currentTopicIdRef: { - get: () => currentTopicId - }, + currentSelectedItemRef, + currentTopicIdRef, }, + topicSelectionReadiness, + listenerOwner: topicListDomListenerOwner, + uiManager: window.uiManager, + itemListManager: window.itemListManager, uiHelper: uiHelperFunctions, mainRendererFunctions: { updateCurrentItemConfig: (newConfig) => { - if (currentSelectedItem.config) { - currentSelectedItem.config = newConfig; - } else { - Object.assign(currentSelectedItem, newConfig); - } + const selectedItem = currentSelectedItemRef.get(); + currentSelectedItemRef.set(selectedItem?.config + ? { ...selectedItem, config: newConfig } + : { ...selectedItem, ...newConfig }); }, handleTopicDeletion: (remainingTopics, deletionContext) => { - if (window.chatManager) { - return window.chatManager.handleTopicDeletion(remainingTopics, deletionContext); + if (chatManager) { + return chatManager.handleTopicDeletion(remainingTopics, deletionContext); } else { console.error('[TopicListManager] chatManager not available for handleTopicDeletion'); } }, selectTopic: (topicId) => { - if (window.chatManager) { - return window.chatManager.selectTopic(topicId); + if (chatManager) { + return chatManager.selectTopic(topicId); } else { console.error('[TopicListManager] chatManager not available for selectTopic'); } @@ -858,34 +741,28 @@ const startupThemeGate = new StartupThemeGate({ } // Initialize ChatManager - if (window.chatManager) { - window.chatManager.init({ + if (chatManager) { + chatManager.init({ + chatContext, + chatRepository, + streamConsumerRegistry: mainChatAdapter?.streamRoutes, + chatDomRenderer: mainChatDomRenderer, electronAPI: chatAPI, uiHelper: uiHelperFunctions, modules: { - messageRenderer: window.messageRenderer, + messageRenderer, itemListManager: window.itemListManager, topicListManager: window.topicListManager, groupRenderer: window.GroupRenderer, + streamManager, + interruptHandler, }, refs: { - currentSelectedItemRef: { - get: () => currentSelectedItem, - set: (val) => { - currentSelectedItem = val; - window.currentSelectedItem = val; - } - }, - currentTopicIdRef: { - get: () => currentTopicId, - set: (val) => { - currentTopicId = val; - window.currentTopicId = val; - } - }, - currentChatHistoryRef: { get: () => currentChatHistory, set: (val) => currentChatHistory = val }, - attachedFilesRef: { get: () => attachedFiles, set: (val) => attachedFiles = val }, - globalSettingsRef: { get: () => globalSettings }, + currentSelectedItemRef, + currentTopicIdRef, + currentChatHistoryRef: mainHistoryRef, + attachedFilesRef: mainChatAttachmentOwner.ref, + globalSettingsRef: mainChatSettingsOwner.ref, }, elements: { chatMessagesDiv: chatMessagesDiv, @@ -898,10 +775,25 @@ const startupThemeGate = new StartupThemeGate({ }, mainRendererFunctions: { displaySettingsForItem: () => window.settingsManager.displaySettingsForItem(), - updateAttachmentPreview: () => uiHelperFunctions.updateAttachmentPreview(attachedFiles, attachmentPreviewArea), + updateAttachmentPreview: mainChatAttachmentOwner.syncPreview, // This is no longer needed as chatManager will call messageRenderer's summarizer - } + }, + notifySendStateChanged: mainChatSendOwner.update }); + ownedRendererSubscriptions.add(chatManager); + ownedRendererSubscriptions.add({ dispose: () => window.topicListManager.dispose?.() }); + window.dispatchEvent(new CustomEvent('vcp-main-chat-commands-configure', { detail: { + chatManager, + capabilities: { + uiHelper: uiHelperFunctions, + uiManager: window.uiManager, + itemListManager: window.itemListManager, + filterManager: window.filterManager, + notificationRenderer: window.notificationRenderer, + appearanceStudio: window.VCPAppearanceStudio, + topTabManager: window.topTabManager, + }, + } })); } else { console.error('[RENDERER_INIT] chatManager module not found!'); } @@ -912,22 +804,11 @@ const startupThemeGate = new StartupThemeGate({ window.settingsManager.init({ electronAPI: chatAPI, uiHelper: uiHelperFunctions, + messageRenderer, refs: { - currentSelectedItemRef: { - get: () => currentSelectedItem, - set: (val) => { - currentSelectedItem = val; - window.currentSelectedItem = val; - } - }, - currentTopicIdRef: { - get: () => currentTopicId, - set: (val) => { - currentTopicId = val; - window.currentTopicId = val; - } - }, - currentChatHistoryRef: { get: () => currentChatHistory, set: (val) => currentChatHistory = val }, + currentSelectedItemRef, + currentTopicIdRef, + currentChatHistoryRef: mainHistoryRef, }, elements: { agentSettingsContainer: document.getElementById('agentSettingsContainer'), @@ -965,7 +846,7 @@ const startupThemeGate = new StartupThemeGate({ getCroppedFile: uiHelperFunctions.getCroppedFile, updateChatHeader: (text) => { if (currentChatNameH3) currentChatNameH3.textContent = text; }, onItemDeleted: async () => { - window.chatManager.displayNoItemSelected(); + chatManager.displayNoItemSelected(); await window.itemListManager.loadItems(); } } @@ -989,15 +870,20 @@ const startupThemeGate = new StartupThemeGate({ startupThemeGate.release({ mode: 'system', message: error?.message || '设置加载失败,已使用系统主题' }); } await window.itemListManager.loadItems(); // Load both agents and groups - await window.chatManager.restoreLastOpenState(globalSettings); + await chatManager.restoreLastOpenState(getGlobalSettings()); // Initialize UI Manager after settings are loaded to ensure correct theme, widths, etc. if (window.uiManager) { + ownedRendererSubscriptions.add({ dispose: () => window.uiManager.dispose?.() }); await window.uiManager.init({ electronAPI: chatAPI, refs: { - globalSettingsRef: { get: () => globalSettings, set: (newSettings) => globalSettings = newSettings }, + globalSettingsRef: mainChatSettingsOwner.ref, }, + listenerOwner: mainChatDomListenerOwner, + settingsManager: window.settingsManager, + itemListManager: window.itemListManager, + uiHelper: uiHelperFunctions, elements: { leftSidebar: document.querySelector('.sidebar'), rightNotificationsSidebar: document.getElementById('notificationsSidebar'), @@ -1020,7 +906,7 @@ const startupThemeGate = new StartupThemeGate({ electronAPI: chatAPI, uiHelper: uiHelperFunctions, refs: { - globalSettingsRef: { get: () => globalSettings, set: (newSettings) => globalSettings = newSettings }, + globalSettingsRef: mainChatSettingsOwner.ref, } }); } else { @@ -1042,23 +928,31 @@ const startupThemeGate = new StartupThemeGate({ contextSanitizerDepthContainer: document.getElementById('contextSanitizerDepthContainer'), addNetworkPathBtn: document.getElementById('addNetworkPathBtn'), refs: { - currentSelectedItem: { get: () => currentSelectedItem }, - currentTopicId: { get: () => currentTopicId }, - globalSettings: { get: () => globalSettings }, - attachedFiles: { get: () => attachedFiles, set: (val) => attachedFiles = val }, - currentChatHistory: { get: () => currentChatHistory, set: (val) => currentChatHistory = val }, + currentSelectedItem: currentSelectedItemRef, + currentTopicId: currentTopicIdRef, + globalSettings: mainChatSettingsOwner.ref, + attachedFiles: mainChatAttachmentOwner.ref, + currentChatHistory: mainHistoryRef, }, uiHelperFunctions, - chatManager: window.chatManager, + chatManager, + messageRenderer, + historyMutationAuthority, itemListManager: window.itemListManager, settingsManager: window.settingsManager, uiManager: window.uiManager, topicListManager: window.topicListManager, getCroppedFile: uiHelperFunctions.getCroppedFile, setCroppedFile: uiHelperFunctions.setCroppedFile, - updateAttachmentPreview: () => uiHelperFunctions.updateAttachmentPreview(attachedFiles, attachmentPreviewArea), + updateAttachmentPreview: mainChatAttachmentOwner.syncPreview, filterAgentList: uiHelperFunctions.filterAgentList, - addNetworkPathInput: uiHelperFunctions.addNetworkPathInput + addNetworkPathInput: uiHelperFunctions.addNetworkPathInput, + sendButtonAction: mainChatSendOwner.handleAction, + normalizeChatPresentationMode, + applyChatPresentationMode, + applyChatBubbleLayoutSettings, + getAppearance: () => window.VCPAppearance, + listenerOwner: mainChatDomListenerOwner }); // A visible DOM shell is not enough to call the desktop interactive: @@ -1080,10 +974,10 @@ const startupThemeGate = new StartupThemeGate({ window.emoticonManager.togglePanel(emoticonTriggerBtn); }; - emoticonTriggerBtn.addEventListener('click', openEmoticonPanel); - emoticonTriggerBtn.addEventListener('contextmenu', openEmoticonPanel); + mainChatDomListenerOwner.add(emoticonTriggerBtn, 'click', openEmoticonPanel); + mainChatDomListenerOwner.add(emoticonTriggerBtn, 'contextmenu', openEmoticonPanel); - const emoticonTriggerObserver = new MutationObserver(syncEmoticonTriggerButton); + const emoticonTriggerObserver = mainChatDomListenerOwner.own(new MutationObserver(syncEmoticonTriggerButton)); emoticonTriggerObserver.observe(attachFileBtn, { attributes: true, attributeFilter: ['disabled'] @@ -1118,13 +1012,13 @@ const startupThemeGate = new StartupThemeGate({ })); }; - quickNewTopicBtn.addEventListener('click', () => forwardCurrentItemAction('click')); - quickNewTopicBtn.addEventListener('contextmenu', (event) => { + mainChatDomListenerOwner.add(quickNewTopicBtn, 'click', () => forwardCurrentItemAction('click')); + mainChatDomListenerOwner.add(quickNewTopicBtn, 'contextmenu', (event) => { event.preventDefault(); forwardCurrentItemAction('contextmenu'); }); - const quickTopicObserver = new MutationObserver(syncQuickNewTopicButton); + const quickTopicObserver = mainChatDomListenerOwner.own(new MutationObserver(syncQuickNewTopicButton)); quickTopicObserver.observe(currentItemActionBtn, { attributes: true, attributeFilter: ['style', 'title'], @@ -1137,21 +1031,22 @@ const startupThemeGate = new StartupThemeGate({ } // Set default view if no item is selected - if (!currentSelectedItem.id) { - window.chatManager.displayNoItemSelected(); + if (!currentSelectedItemRef.get()?.id) { + chatManager.displayNoItemSelected(); } // Initialize Search Manager if (searchManager) { searchManager.init({ electronAPI: chatAPI, + chatRepository, uiHelper: uiHelperFunctions, refs: { - currentSelectedItemRef: { get: () => currentSelectedItem }, - currentTopicIdRef: { get: () => currentTopicId }, + currentSelectedItemRef, + currentTopicIdRef, }, modules: { - chatManager: window.chatManager, + chatManager, } }); } else { @@ -1159,26 +1054,27 @@ const startupThemeGate = new StartupThemeGate({ } // Emoticon URL fixer is now initialized within messageRenderer - window.__vcpRendererReady = true; + topicSelectionReadiness.setReady(true); - chatAPI.toggleSelectionListener(!!globalSettings.assistantEnabled); + chatAPI.toggleSelectionListener(!!getGlobalSettings().assistantEnabled); - if (window.__vcpPendingTopicSelection && window.chatManager) { - const pending = window.__vcpPendingTopicSelection; - window.__vcpPendingTopicSelection = null; + const pendingTopicSelection = topicSelectionReadiness.takePending(); + if (pendingTopicSelection && chatManager) { + const pending = pendingTopicSelection; + const selectedItem = currentSelectedItemRef.get(); const matchesCurrentItem = - currentSelectedItem && - currentSelectedItem.id === pending.itemId && - currentSelectedItem.type === pending.itemType; + selectedItem && + selectedItem.id === pending.itemId && + selectedItem.type === pending.itemType; if (matchesCurrentItem) { - Promise.resolve(window.chatManager.selectTopic(pending.topicId)).catch((error) => { + Promise.resolve(chatManager.selectTopic(pending.topicId)).catch((error) => { console.error('[Renderer] Failed to replay pending topic selection:', error); }); } } } catch (error) { - window.__vcpRendererReady = false; + topicSelectionReadiness.setReady(false); console.error('Error during DOMContentLoaded initialization:', error); startupThemeGate.release({ mode: 'system', @@ -1191,10 +1087,13 @@ const startupThemeGate = new StartupThemeGate({ // --- Agent Settings Reload Listener --- if (chatAPI?.onReloadAgentSettings) { - chatAPI.onReloadAgentSettings(async ({ agentId }) => { + ownedRendererSubscriptions.add(createOwnedPreloadSubscription({ + subscribe: chatAPI.onReloadAgentSettings, + consume: async ({ agentId }, lifecycle) => { console.log('[Renderer] Received reload-agent-settings event for agent:', agentId); if (window.settingsManager && typeof window.settingsManager.reloadAgentSettings === 'function') { const result = await window.settingsManager.reloadAgentSettings(agentId); + if (!lifecycle.isActive()) return; if (result.success && !result.skipped) { console.log('[Renderer] Agent settings reloaded successfully'); uiHelperFunctions.showToastNotification('设置已自动更新', 'success'); @@ -1202,1321 +1101,73 @@ const startupThemeGate = new StartupThemeGate({ console.log('[Renderer] Agent settings reload skipped (not currently editing)'); } } - }); + }, + })); console.log('[Renderer] Agent settings reload listener initialized'); } // --- TTS Audio Playback and Visuals --- - setupTtsListeners(); + ttsSurfaceOwner.mount(); + ownedRendererSubscriptions.add({ dispose: () => ttsSurfaceOwner.dispose() }); // --- File Watcher Listener --- - chatAPI.onHistoryFileUpdated(({ agentId, topicId, path }) => { - if (currentSelectedItem && currentSelectedItem.id === agentId && currentTopicId === topicId) { + ownedRendererSubscriptions.add(createOwnedPreloadSubscription({ + subscribe: chatAPI.onHistoryFileUpdated, + consume: async ({ agentId, topicId, path }, lifecycle) => { + const selectedItem = currentSelectedItemRef.get(); + if (selectedItem?.id === agentId && currentTopicIdRef.get() === topicId) { console.log('[Renderer] Active chat history was modified externally. Syncing...'); + if (!lifecycle.isActive()) return; uiHelperFunctions.showToastNotification("聊天记录已同步。", "info"); - if (window.chatManager && typeof window.chatManager.syncHistoryFromFile === 'function') { - window.chatManager.syncHistoryFromFile(agentId, currentSelectedItem.type, topicId); + if (chatManager && typeof chatManager.syncHistoryFromFile === 'function') { + await chatManager.syncHistoryFromFile(agentId, selectedItem.type, topicId); } } - }); + }, + })); // --- Initialize Flowlock Module --- if (window.initializeFlowlockIntegration) { - window.initializeFlowlockIntegration(); + window.initializeFlowlockIntegration({ + chatManager, + historyMutationAuthority, + settings: mainChatSettingsOwner, + listenerOwner: mainChatDomListenerOwner, + }); console.log('[Renderer] Flowlock integration initialized.'); } else { console.warn('[Renderer] Flowlock integration function not found.'); } - // --- Listen for Flowlock commands from plugins (via main process) --- - if (chatAPI?.onFlowlockCommand) { - const respondToFlowlockRequest = (commandData, responseData) => { - if (!commandData?.requestId || !chatAPI?.sendFlowlockRpcResponse) { - return; - } - - chatAPI.sendFlowlockRpcResponse({ - requestId: commandData.requestId, - ok: responseData?.success !== false, - data: responseData?.success === false ? undefined : responseData, - error: responseData?.success === false ? responseData.error : undefined, - }); - }; - - const flowlockCommandHandler = async (commandData) => { - console.log('[Renderer] Received flowlock command from plugin:', commandData); - - if (!window.flowlockManager) { - console.error('[Renderer] flowlockManager not available'); - respondToFlowlockRequest(commandData, { - command: commandData?.command, - success: false, - error: 'flowlockManager not available' - }); - return; - } - - const { command, agentId, topicId, prompt, promptSource } = commandData; - const targetAgentId = agentId || currentSelectedItem?.id; - - try { - switch (command) { - case 'start': - // 兼容入口:内部 Session Map 仍是唯一运行状态源。 - if (targetAgentId && topicId) { - await window.flowlockManager.start(targetAgentId, topicId, { startImmediately: false }); - console.log(`[Renderer] Flowlock started for agent: ${targetAgentId}, topic: ${topicId}`); - } else { - console.error('[Renderer] Missing agentId or topicId for start command'); - } - break; - - case 'stop': - if (targetAgentId) { - await window.flowlockManager.stop(targetAgentId); - console.log(`[Renderer] Flowlock stopped for agent: ${targetAgentId}`); - } - break; - - case 'promptee': - if (targetAgentId && prompt) { - window.flowlockManager.setCustomPrompt(targetAgentId, prompt); - console.log(`[Renderer] Flowlock next prompt set for agent: ${targetAgentId}`); - } else { - console.error('[Renderer] Missing target agent or prompt for promptee command'); - } - break; - - case 'prompter': - // Get content from external source and append to input - if (promptSource) { - // TODO: Implement fetching from external source - // For now, just log the source - console.log(`[Renderer] Prompter source: ${promptSource}`); - // Placeholder: treat promptSource as the actual prompt for now - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - const currentValue = messageInput.value; - messageInput.value = currentValue + (currentValue ? ' ' : '') + `[来自: ${promptSource}]`; - console.log(`[Renderer] Prompter content appended from source: ${promptSource}`); - // Auto-resize textarea after content change - if (window.uiHelperFunctions && window.uiHelperFunctions.autoResizeTextarea) { - window.uiHelperFunctions.autoResizeTextarea(messageInput); - } - } - } else { - console.error('[Renderer] Missing promptSource for prompter command'); - } - break; - - case 'clear': - // Clear all content in input box - { - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - messageInput.value = ''; - console.log('[Renderer] Input box cleared'); - // Auto-resize textarea after content change - if (window.uiHelperFunctions && window.uiHelperFunctions.autoResizeTextarea) { - window.uiHelperFunctions.autoResizeTextarea(messageInput); - } - } - } - break; - - case 'remove': - // Remove specific text from input - { - const { target } = commandData; - if (target) { - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - const currentValue = messageInput.value; - // Remove all occurrences of target text - messageInput.value = currentValue.split(target).join(''); - console.log(`[Renderer] Removed "${target}" from input`); - // Auto-resize textarea after content change - if (window.uiHelperFunctions && window.uiHelperFunctions.autoResizeTextarea) { - window.uiHelperFunctions.autoResizeTextarea(messageInput); - } - } - } else { - console.error('[Renderer] Missing target for remove command'); - } - } - break; - - case 'edit': - // Edit (diff) specific text in input - find oldText and replace with newText - { - const { oldText, newText } = commandData; - if (oldText && newText !== undefined) { - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - const currentValue = messageInput.value; - // Replace first occurrence only (diff-style) - const index = currentValue.indexOf(oldText); - if (index !== -1) { - messageInput.value = currentValue.substring(0, index) + newText + currentValue.substring(index + oldText.length); - console.log(`[Renderer] Edited text: "${oldText}" → "${newText}"`); - // Auto-resize textarea after content change - if (window.uiHelperFunctions && window.uiHelperFunctions.autoResizeTextarea) { - window.uiHelperFunctions.autoResizeTextarea(messageInput); - } - } else { - console.warn(`[Renderer] oldText "${oldText}" not found in input`); - } - } - } else { - console.error('[Renderer] Missing oldText or newText for edit command'); - } - } - break; - - case 'get': - // Get current input box content and return it - { - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - const content = messageInput.value; - console.log(`[Renderer] Retrieved input box content: "${content}"`); - respondToFlowlockRequest(commandData, { - command: 'get', - success: true, - content: content - }); - } else { - console.error('[Renderer] Message input element not found'); - respondToFlowlockRequest(commandData, { - command: 'get', - success: false, - error: 'Message input element not found' - }); - } - } - break; - - case 'status': - { - if (window.flowlockManager) { - const state = targetAgentId - ? window.flowlockManager.getSession(targetAgentId) - : null; - const activeAgents = window.flowlockManager.getActiveAgents(); - console.log('[Renderer] Retrieved flowlock status:', { state, activeAgents }); - respondToFlowlockRequest(commandData, { - command: 'status', - success: true, - status: { - isActive: state?.status === 'active', - isProcessing: !!state?.activeMessageId, - agentId: state?.agentId || targetAgentId || null, - topicId: state?.topicId || null, - session: state, - activeAgents - } - }); - } else { - console.error('[Renderer] flowlockManager not available'); - respondToFlowlockRequest(commandData, { - command: 'status', - success: false, - error: 'flowlockManager not available' - }); - } - } - break; - - default: - console.error(`[Renderer] Unknown flowlock command: ${command}`); - } - } catch (error) { - console.error('[Renderer] Error executing flowlock command:', error); - } - }; - - chatAPI.onFlowlockCommand(flowlockCommandHandler); - if (chatAPI.onFlowlockRequest) { - chatAPI.onFlowlockRequest(flowlockCommandHandler); - } - console.log('[Renderer] Flowlock command listener initialized'); - } - -}); - -function setupTtsListeners() { - // This function is now called from ensureAudioContext, not on body events - const initAudioContext = () => { - if (!audioContext) { - try { - audioContext = new (window.AudioContext || window.webkitAudioContext)(); - console.log("[TTS Renderer] AudioContext initialized successfully."); - return true; - } catch (e) { - console.error("[TTS Renderer] Failed to initialize AudioContext:", e); - uiHelperFunctions.showToastNotification("无法初始化音频播放器。", "error"); - return false; - } - } - return true; - }; - - // Expose a function to be called on demand - window.ensureAudioContext = initAudioContext; - - // 新的TTS播放逻辑:使用sessionId来处理异步时序问题 - chatAPI.onPlayTtsAudio(async ({ audioData, msgId, sessionId }) => { - // 如果收到的sessionId小于当前的,说明是过时的事件,直接忽略 - if (sessionId < currentTtsSessionId) { - console.log(`[TTS Renderer] Discarding stale audio data from old session ${sessionId}. Current session is ${currentTtsSessionId}.`); - return; - } - - // 如果sessionId大于当前的,说明是一个全新的播放请求 - if (sessionId > currentTtsSessionId) { - console.log(`[TTS Renderer] New TTS session ${sessionId} started. Clearing old queue.`); - currentTtsSessionId = sessionId; - // 清空队列,扔掉所有可能属于更旧会话的音频块 - ttsAudioQueue = []; - } - - // 只有当sessionId匹配时,才将音频加入队列 - console.log(`[TTS Renderer] Received audio data for msgId ${msgId} (session ${sessionId}). Pushing to queue.`); - if (!audioContext) { - console.warn("[TTS Renderer] AudioContext not initialized. Buffering audio but cannot play yet."); - } - ttsAudioQueue.push({ audioData, msgId }); - processTtsQueue(); // 尝试处理队列 - }); - - async function processTtsQueue() { - if (isTtsPlaying || ttsAudioQueue.length === 0) { - // 如果队列为空且没有在播放,确保关闭所有动画 - if (!isTtsPlaying && currentPlayingMsgId) { - uiHelperFunctions.updateSpeakingIndicator(currentPlayingMsgId, false); - currentPlayingMsgId = null; - } - return; - } - - if (!audioContext) { - console.warn("[TTS Renderer] AudioContext not ready. Waiting to process TTS queue."); - return; - } - - isTtsPlaying = true; - const { audioData, msgId } = ttsAudioQueue.shift(); - - // 更新UI动画 - if (currentPlayingMsgId !== msgId) { - // 关闭上一个正在播放的动画(如果有) - if (currentPlayingMsgId) { - uiHelperFunctions.updateSpeakingIndicator(currentPlayingMsgId, false); - } - // 开启当前新的动画 - currentPlayingMsgId = msgId; - uiHelperFunctions.updateSpeakingIndicator(currentPlayingMsgId, true); - } - - try { - const audioBuffer = await audioContext.decodeAudioData( - Uint8Array.from(atob(audioData), c => c.charCodeAt(0)).buffer - ); - - // 关键修复:在异步解码后,再次检查停止标志,防止竞态条件 - if (!isTtsPlaying) { - console.log("[TTS Renderer] Stop command received during audio decoding. Aborting playback."); - // onStopTtsAudio已经处理了状态重置,这里只需中止即可 - return; - } - - currentAudioSource = audioContext.createBufferSource(); - currentAudioSource.buffer = audioBuffer; - currentAudioSource.connect(audioContext.destination); - - currentAudioSource.onended = () => { - console.log(`[TTS Renderer] Playback finished for a chunk of msgId ${msgId}.`); - isTtsPlaying = false; - currentAudioSource = null; - processTtsQueue(); // 播放下一个 - }; - - currentAudioSource.start(0); - console.log(`[TTS Renderer] Starting playback for a chunk of msgId ${msgId}.`); - - } catch (error) { - console.error("[TTS Renderer] Error decoding or playing TTS audio from queue:", error); - uiHelperFunctions.showToastNotification(`播放音频失败: ${error.message}`, "error"); - isTtsPlaying = false; - processTtsQueue(); // 即使失败也尝试处理下一个 - } - } - - chatAPI.onStopTtsAudio(() => { - console.error("!!!!!!!!!! [TTS RENDERER] STOP EVENT RECEIVED !!!!!!!!!!"); - - // 关键:增加会话ID,使所有后续到达的、属于旧会话的play-tts-audio事件全部失效 - currentTtsSessionId++; - console.log(`[TTS Renderer] Stop event incremented session ID to ${currentTtsSessionId}.`); - - console.log("Clearing TTS queue, stopping current audio source, and resetting state."); - - ttsAudioQueue = []; // 1. 清空前端队列 - - if (currentAudioSource) { - console.log("Found active audio source. Stopping it now."); - currentAudioSource.onended = null; // 2. 阻止onended回调 - currentAudioSource.stop(); // 3. 停止当前音频 - currentAudioSource = null; - } else { - console.warn("Stop event received, but no active audio source was found."); - } - - isTtsPlaying = false; // 4. 重置播放状态标志 - - // 5. 确保关闭当前的播放动画 - if (currentPlayingMsgId) { - console.log(`Closing speaking indicator for message ID: ${currentPlayingMsgId}`); - uiHelperFunctions.updateSpeakingIndicator(currentPlayingMsgId, false); - currentPlayingMsgId = null; - } - }); - - // 移除旧的 onSovitsStatusChanged 监听器,因为它不再准确 - // window.electronAPI.onSovitsStatusChanged(...) - - // This function has been moved to modules/ui-helpers.js -} - -// This function has been moved to modules/ui-helpers.js - + // Flowlock command/request ownership is provided by MainChatFlowlockOwner. -async function loadAndApplyGlobalSettings() { - let settings; - try { - settings = await loadSettingsWithTimeout( - chatAPI?.loadSettings, - STARTUP_SETTINGS_TIMEOUT_MS, - '加载设置超时' - ); - } catch (error) { - startupThemeGate.release({ mode: 'system', message: error?.message || '设置加载失败' }); - throw error; - } - if (settings && !settings.error) { - globalSettings = { ...globalSettings, ...settings }; - window.globalSettings = globalSettings; - - // Theme variables must be selected before any item/chat restoration can paint. - // The uiManager listener is initialized later and receives the same effective value. - startupThemeGate.release({ mode: globalSettings.currentThemeMode }); - - globalSettings.appearanceProfile = window.VCPAppearance?.commit( - globalSettings.appearanceProfile, - { uiMode: 'next', source: 'settings-load' } - ) || globalSettings.appearanceProfile; - window.dispatchEvent(new CustomEvent('global-settings-updated', { - detail: { settings: globalSettings, source: 'settings-load' } - })); - applyChatBubbleLayoutSettings(globalSettings); - - // 🟢 优化:仅更新始终存在的 UI 元素 - if (globalSettings.sidebarWidth && leftSidebar) { - leftSidebar.style.width = `${globalSettings.sidebarWidth}px`; - } - if (leftSidebar) { - const sidebarIsActive = globalSettings.sidebarActive !== false; - const avatarOnly = sidebarIsActive && globalSettings.sidebarAvatarOnly === true; - leftSidebar.classList.toggle('active', sidebarIsActive); - leftSidebar.classList.toggle('avatar-only', avatarOnly); - document.querySelector('.main-content')?.classList.toggle('sidebar-active', sidebarIsActive); - } - if (globalSettings.notificationsSidebarWidth && rightNotificationsSidebar) { - if (rightNotificationsSidebar.classList.contains('active')) { - rightNotificationsSidebar.style.width = `${globalSettings.notificationsSidebarWidth}px`; - } - } - - if (globalSettings.vcpLogUrl && globalSettings.vcpLogKey) { - if (window.notificationRenderer) window.notificationRenderer.updateVCPLogStatus({ status: 'connecting', message: '连接中...' }, vcpLogConnectionStatusDiv); - chatAPI.connectVCPLog(globalSettings.vcpLogUrl, globalSettings.vcpLogKey); - } else { - if (window.notificationRenderer) window.notificationRenderer.updateVCPLogStatus({ status: 'error', message: 'VCPLog未配置' }, vcpLogConnectionStatusDiv); - } - - // Set the initial state of the new toggle button in the main UI - if (toggleAssistantBtn) { - toggleAssistantBtn.classList.toggle('active', !!globalSettings.assistantEnabled); - } - if (toggleSidebarModeBtn) { - const avatarOnly = globalSettings.sidebarActive !== false && globalSettings.sidebarAvatarOnly === true; - toggleSidebarModeBtn.classList.toggle('active', avatarOnly); - toggleSidebarModeBtn.setAttribute('aria-pressed', String(avatarOnly)); - } - // Selection listener startup moved to post-renderer-ready stage. - - // Load filter mode setting - let filterEnabled = globalSettings.filterEnabled ?? globalSettings.doNotDisturbLogMode ?? (localStorage.getItem('doNotDisturbLogMode') === 'true'); - globalSettings.filterEnabled = filterEnabled; - // 🟢 核心逻辑:监听模态框就绪事件,届时再同步模态框内部 UI - document.addEventListener('modal-ready', (e) => { - const { modalId } = e.detail; - if (modalId === 'globalSettingsModal') { - syncGlobalSettingsToUI(); - } - }); - - if (window.messageRenderer) { - window.messageRenderer.setUserAvatar(globalSettings.userAvatarUrl); - window.messageRenderer.setUserAvatarColor(globalSettings.userAvatarCalculatedColor); - } - } else { - console.warn('加载全局设置失败或无设置:', settings?.error); - startupThemeGate.release({ mode: 'system', message: settings?.error || '设置加载失败,已使用系统主题' }); - if (window.notificationRenderer) window.notificationRenderer.updateVCPLogStatus({ status: 'error', message: 'VCPLog未配置' }, vcpLogConnectionStatusDiv); - } -} -function applyInitialThemeClass(mode) { - let effectiveTheme = mode === 'light' || mode === 'dark' ? mode : null; - if (!effectiveTheme && typeof window.matchMedia === 'function') { - effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - } - effectiveTheme = effectiveTheme || 'light'; - if (window.uiManager?.applyTheme) { - window.uiManager.applyTheme(effectiveTheme); - } else { - document.body.classList.remove('light-theme', 'dark-theme'); - document.body.classList.add(`${effectiveTheme}-theme`); - } - document.body.removeAttribute('data-theme-pending'); -} - -const CHAT_PRESENTATION_MODES = Object.freeze(['bubble', 'panel', 'immersive']); -const CHAT_PRESENTATION_MODE_CLASSES = CHAT_PRESENTATION_MODES.map( - mode => `chat-presentation-${mode}` -); - -function normalizeChatPresentationMode(mode) { - return CHAT_PRESENTATION_MODES.includes(mode) ? mode : 'bubble'; -} - -function isChatVisuallyNearBottom(scrollContainer, threshold = 64) { - const messages = scrollContainer?.querySelector('.chat-messages'); - if (!scrollContainer || !messages) return true; - - const containerRect = scrollContainer.getBoundingClientRect(); - const messagesRect = messages.getBoundingClientRect(); - return Math.abs(messagesRect.bottom - containerRect.bottom) <= threshold; -} - -function captureChatPresentationScrollAnchor() { - const scrollContainer = document.querySelector('.chat-messages-container'); - if (!scrollContainer) return null; - - if (isChatVisuallyNearBottom(scrollContainer)) { - return { scrollContainer, stickToBottom: true }; - } - - const containerRect = scrollContainer.getBoundingClientRect(); - const visibleMessages = Array.from( - scrollContainer.querySelectorAll('.message-item[data-message-id]') - ) - .map(element => ({ element, rect: element.getBoundingClientRect() })) - .filter(({ rect }) => rect.bottom > containerRect.top && rect.top < containerRect.bottom) - .sort((a, b) => a.rect.top - b.rect.top); - - const anchor = visibleMessages[0]; - if (!anchor) { - return { - scrollContainer, - stickToBottom: false, - scrollTop: scrollContainer.scrollTop - }; - } - - return { - scrollContainer, - stickToBottom: false, - messageId: anchor.element.dataset.messageId, - viewportOffset: anchor.rect.top - containerRect.top, - scrollTop: scrollContainer.scrollTop - }; -} - -function restoreChatPresentationScrollAnchor(anchor) { - if (!anchor?.scrollContainer?.isConnected) return; - - const { scrollContainer } = anchor; - if (anchor.stickToBottom) { - const messages = scrollContainer.querySelector('.chat-messages'); - if (!messages) return; - const containerRect = scrollContainer.getBoundingClientRect(); - const messagesRect = messages.getBoundingClientRect(); - scrollContainer.scrollTop += messagesRect.bottom - containerRect.bottom; - return; - } - - const anchorElement = anchor.messageId - ? scrollContainer.querySelector(`.message-item[data-message-id="${CSS.escape(anchor.messageId)}"]`) - : null; - - if (!anchorElement) { - scrollContainer.scrollTop = anchor.scrollTop; - return; - } - - const containerRect = scrollContainer.getBoundingClientRect(); - const currentOffset = anchorElement.getBoundingClientRect().top - containerRect.top; - scrollContainer.scrollTop += currentOffset - anchor.viewportOffset; -} - -let chatPresentationSaveGeneration = 0; -let chatPresentationSaveChain = Promise.resolve(); - -function syncChatPresentationModeControls(mode = globalSettings.chatPresentationMode) { - const normalizedMode = normalizeChatPresentationMode(mode); - const modeControl = document.querySelector( - `input[name="chatPresentationMode"][value="${normalizedMode}"]` - ); - if (modeControl) modeControl.checked = true; - - document.querySelectorAll('.chat-presentation-quick-option').forEach((option) => { - const isActive = option.dataset.presentationMode === normalizedMode; - option.classList.toggle('active', isActive); - option.setAttribute('aria-checked', String(isActive)); - option.tabIndex = isActive ? 0 : -1; - }); - - const bubbleOnlySettings = document.getElementById('userChatBubbleSettings'); - if (bubbleOnlySettings) { - bubbleOnlySettings.hidden = normalizedMode !== 'bubble'; - } - - const bubbleWidthSettings = document.getElementById('chatBubbleWidthSettings'); - if (bubbleWidthSettings) { - bubbleWidthSettings.hidden = normalizedMode !== 'bubble'; - } -} - -function setupChatPresentationQuickSwitcher() { - document.querySelectorAll('.chat-presentation-quick-switcher').forEach((switcher) => { - const options = Array.from(switcher.querySelectorAll('.chat-presentation-quick-option')); - if (!options.length || switcher.dataset.bound === 'true') return; - const trigger = document.querySelector(`[aria-controls="${switcher.id}"]`); - const usesExplicitState = switcher.classList.contains('next-ui-chat-presentation-switcher'); - - const setOpen = (open, { restoreFocus = false } = {}) => { - switcher.classList.toggle('is-open', open); - trigger?.setAttribute('aria-expanded', String(open)); - switcher.setAttribute('aria-hidden', String(!open)); - switcher.inert = !open; - if (!open && restoreFocus) trigger?.focus(); - }; - - const selectMode = async (option) => { - const mode = option?.dataset.presentationMode; - if (!mode) return; - await applyChatPresentationMode(mode, { - persist: true, - preserveScroll: true, - notify: false, - source: `${switcher.id || 'chat-presentation'}-quick-switcher` - }); - }; - - if (usesExplicitState && trigger) { - trigger.addEventListener('click', (event) => { - event.stopPropagation(); - const open = !switcher.classList.contains('is-open'); - setOpen(open); - if (open) { - options.find(option => option.getAttribute('aria-checked') === 'true')?.focus(); - } - }); - document.addEventListener('pointerdown', (event) => { - if (!switcher.classList.contains('is-open')) return; - if (switcher.contains(event.target) || trigger.contains(event.target)) return; - setOpen(false); - }); - document.addEventListener('keydown', (event) => { - if (event.key !== 'Escape' || !switcher.classList.contains('is-open')) return; - event.preventDefault(); - event.stopPropagation(); - setOpen(false, { restoreFocus: true }); - }); - } - - options.forEach((option) => { - option.addEventListener('click', async () => { - await selectMode(option); - if (usesExplicitState) setOpen(false, { restoreFocus: true }); - }); - option.addEventListener('keydown', (event) => { - if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; - event.preventDefault(); - - const currentIndex = Math.max(0, options.indexOf(option)); - let nextIndex = currentIndex; - if (event.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + options.length) % options.length; - if (event.key === 'ArrowRight') nextIndex = (currentIndex + 1) % options.length; - if (event.key === 'Home') nextIndex = 0; - if (event.key === 'End') nextIndex = options.length - 1; - - options[nextIndex].focus(); - selectMode(options[nextIndex]); - }); - }); - - switcher.addEventListener('keydown', (event) => { - if (event.key !== 'Escape') return; - event.preventDefault(); - event.stopPropagation(); - if (usesExplicitState) setOpen(false); - trigger?.focus(); - if (!usesExplicitState) trigger?.blur(); - }); - - switcher.dataset.bound = 'true'; - setOpen(false); - }); - syncChatPresentationModeControls(globalSettings.chatPresentationMode); -} - -async function applyChatPresentationMode(mode, options = {}) { - const { - persist = false, - preserveScroll = true, - notify = false, - source = 'unknown' - } = options; - const normalizedMode = normalizeChatPresentationMode(mode); - const previousMode = normalizeChatPresentationMode(globalSettings.chatPresentationMode); - const generation = persist ? ++chatPresentationSaveGeneration : chatPresentationSaveGeneration; - const anchor = preserveScroll ? captureChatPresentationScrollAnchor() : null; - - document.body?.classList.remove(...CHAT_PRESENTATION_MODE_CLASSES); - document.body?.classList.add(`chat-presentation-${normalizedMode}`); - globalSettings.chatPresentationMode = normalizedMode; - window.globalSettings = globalSettings; - syncChatPresentationModeControls(normalizedMode); - - if (window.pretextBridge?.setPresentationMode) { - window.pretextBridge.setPresentationMode(normalizedMode); - } else { - window.pretextBridge?.clearAll?.(); - } - window.messageRenderer?.refreshLayoutDependentState?.(); - - if (anchor) { - requestAnimationFrame(() => { - requestAnimationFrame(() => restoreChatPresentationScrollAnchor(anchor)); - }); - } - - if (!persist || normalizedMode === previousMode) { - return { success: true, mode: normalizedMode }; - } - - try { - chatPresentationSaveChain = chatPresentationSaveChain - .catch(() => undefined) - .then(() => chatAPI.saveSettings({ chatPresentationMode: normalizedMode })); - const result = await chatPresentationSaveChain; - if (!result?.success) { - throw new Error(result?.error || '设置保存失败'); - } - if (notify) { - uiHelperFunctions?.showToastNotification?.('聊天显示模式已切换。', 'success'); - } - return { success: true, mode: normalizedMode, source }; - } catch (error) { - if (generation === chatPresentationSaveGeneration) { - await applyChatPresentationMode(previousMode, { - persist: false, - preserveScroll: true, - notify: false, - source: 'rollback' - }); - } - if (generation === chatPresentationSaveGeneration) { - uiHelperFunctions?.showToastNotification?.(`聊天显示模式保存失败:${error.message}`, 'error'); - } - return { success: false, mode: previousMode, error }; - } -} - -function clampChatBubbleWidthPercent(value, fallback) { - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed)) return fallback; - return Math.min(98, Math.max(50, parsed)); -} - -const CHAT_FONT_PRESETS = Object.freeze({ - system: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - segoe: '"Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif', - ubuntu: '"Ubuntu", "Segoe UI", "Microsoft YaHei UI", sans-serif', - yahei: '"Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI", sans-serif', - pingfang: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", sans-serif', - 'source-han': '"Source Han Sans SC", "Noto Sans CJK SC", "Microsoft YaHei UI", sans-serif', - serif: '"Noto Serif SC", "Source Han Serif SC", "Songti SC", Georgia, serif' -}); - -const CHAT_CODE_FONT_PRESETS = Object.freeze({ - cascadia: '"Cascadia Code", "Consolas", "JetBrains Mono", monospace', - fira: '"Fira Code", "Consolas", "JetBrains Mono", monospace', - consolas: '"Consolas", "Monaco", "Courier New", monospace', - system: 'ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - jetbrains: '"JetBrains Mono", "Cascadia Code", "Fira Code", "Consolas", monospace', - monaspace: '"Monaspace Neon", "JetBrains Mono", "Cascadia Code", monospace' -}); - -const TOOL_CARD_FONT_PRESETS = Object.freeze({ - ...CHAT_FONT_PRESETS, - cascadia: CHAT_CODE_FONT_PRESETS.cascadia, - fira: CHAT_CODE_FONT_PRESETS.fira, - consolas: CHAT_CODE_FONT_PRESETS.consolas, - jetbrains: CHAT_CODE_FONT_PRESETS.jetbrains, - monaspace: CHAT_CODE_FONT_PRESETS.monaspace -}); - -function sanitizeFontFamilyValue(value) { - if (typeof value !== 'string') return ''; - return value.trim().replace(/[\r\n]+/g, ' '); -} - -function resolveFontFamilyFromPreset(presetMap, presetKey, customValue, fallbackKey) { - const normalizedPreset = typeof presetKey === 'string' ? presetKey : fallbackKey; - if (normalizedPreset === 'custom') { - return sanitizeFontFamilyValue(customValue) || presetMap[fallbackKey]; - } - return presetMap[normalizedPreset] || presetMap[fallbackKey]; -} - -function resolveChatFontFamily(settings = globalSettings) { - return resolveFontFamilyFromPreset( - CHAT_FONT_PRESETS, - settings?.chatFontPreset, - settings?.chatFontCustom, - 'system' - ); -} - -function resolveChatCodeFontFamily(settings = globalSettings) { - return resolveFontFamilyFromPreset( - CHAT_CODE_FONT_PRESETS, - settings?.chatCodeFontPreset, - settings?.chatCodeFontCustom, - 'consolas' - ); -} - -function resolveDiaryFontFamily(settings = globalSettings) { - return resolveFontFamilyFromPreset( - CHAT_FONT_PRESETS, - settings?.chatDiaryFontPreset, - settings?.chatDiaryFontCustom, - 'serif' - ); -} - -function resolveToolFontFamily(settings = globalSettings) { - return resolveFontFamilyFromPreset( - TOOL_CARD_FONT_PRESETS, - settings?.chatToolFontPreset, - settings?.chatToolFontCustom, - 'system' - ); -} - -function syncChatFontControl(selectId, customRowId) { - const presetSelect = document.getElementById(selectId); - const customRow = document.getElementById(customRowId); - if (!presetSelect || !customRow) return; - customRow.style.display = presetSelect.value === 'custom' ? 'block' : 'none'; -} - -function updateFontScenarioPreview() { - const bodyFontFamily = resolveChatFontFamily({ - chatFontPreset: document.getElementById('chatFontPreset')?.value || 'system', - chatFontCustom: document.getElementById('chatFontCustom')?.value || '' - }); - const codeFontFamily = resolveChatCodeFontFamily({ - chatCodeFontPreset: document.getElementById('chatCodeFontPreset')?.value || 'consolas', - chatCodeFontCustom: document.getElementById('chatCodeFontCustom')?.value || '' - }); - const diaryFontFamily = resolveDiaryFontFamily({ - chatDiaryFontPreset: document.getElementById('chatDiaryFontPreset')?.value || 'serif', - chatDiaryFontCustom: document.getElementById('chatDiaryFontCustom')?.value || '' - }); - const toolFontFamily = resolveToolFontFamily({ - chatToolFontPreset: document.getElementById('chatToolFontPreset')?.value || 'system', - chatToolFontCustom: document.getElementById('chatToolFontCustom')?.value || '' - }); - - const bodyEl = document.getElementById('scenarioPreviewBody'); - const codeEl = document.getElementById('scenarioPreviewCode'); - const diaryEl = document.getElementById('scenarioPreviewDiary'); - const toolEl = document.getElementById('scenarioPreviewTool'); - - if (bodyEl) bodyEl.style.fontFamily = bodyFontFamily; - if (codeEl) codeEl.style.fontFamily = codeFontFamily; - if (diaryEl) diaryEl.style.fontFamily = diaryFontFamily; - if (toolEl) toolEl.style.fontFamily = toolFontFamily; -} - -function ensureScenarioFontSettingsMount(previewId, mountId) { - const previewEl = document.getElementById(previewId); - if (!previewEl) return null; - - const card = previewEl.closest('.scenario-preview-card'); - if (!card) return null; - - let mountEl = document.getElementById(mountId); - if (!mountEl) { - mountEl = document.createElement('div'); - mountEl.id = mountId; - mountEl.className = 'scenario-preview-settings-slot'; - const noteEl = card.querySelector('.scenario-preview-note'); - if (noteEl) { - card.insertBefore(mountEl, noteEl); - } else { - card.appendChild(mountEl); - } - } - - return mountEl; -} - -function mountChatFontSettingGroups() { - [ - { - groupId: 'chatFontSettingsGroup', - previewId: 'scenarioPreviewBody', - mountId: 'chatFontSettingsMount' + const flowlockOwner = createMainChatFlowlockOwner({ + subscriptions: { command: chatAPI?.onFlowlockCommand, request: chatAPI?.onFlowlockRequest }, + flowlockManager: window.flowlockManager, + getSelectedItem: currentSelectedItemRef.get, + messageInput, + uiHelperFunctions: window.uiHelperFunctions, + sendResponse: (commandData, responseData) => { + if (!commandData?.requestId || !chatAPI?.sendFlowlockRpcResponse) return; + chatAPI.sendFlowlockRpcResponse({ requestId: commandData.requestId, ok: responseData?.success !== false, data: responseData?.success === false ? undefined : responseData, error: responseData?.success === false ? responseData.error : undefined }); }, - { - groupId: 'chatCodeFontSettingsGroup', - previewId: 'scenarioPreviewCode', - mountId: 'chatCodeFontSettingsMount' - } - ].forEach(({ groupId, previewId, mountId }) => { - const groupEl = document.getElementById(groupId); - const mountEl = ensureScenarioFontSettingsMount(previewId, mountId); - if (!groupEl || !mountEl) return; - - if (groupEl.parentElement !== mountEl) { - mountEl.appendChild(groupEl); - } - - groupEl.style.marginTop = '0'; - groupEl.style.marginBottom = '0'; }); -} + flowlockOwner.mount(); + ownedRendererSubscriptions.add(flowlockOwner); -function syncChatFontControls() { - mountChatFontSettingGroups(); - syncChatFontControl('chatFontPreset', 'chatFontCustomRow'); - syncChatFontControl('chatCodeFontPreset', 'chatCodeFontCustomRow'); - syncChatFontControl('chatDiaryFontPreset', 'chatDiaryFontCustomRow'); - syncChatFontControl('chatToolFontPreset', 'chatToolFontCustomRow'); - updateFontScenarioPreview(); -} - -function syncWideChatLayoutControls() { - const wideModeRadio = document.getElementById('chatLayoutModeWide'); - const widthSettings = document.getElementById('chatBubbleWidthSettings'); - if (!wideModeRadio || !widthSettings) return; - widthSettings.style.display = wideModeRadio.checked ? 'block' : 'none'; -} - -function syncUserChatBubbleControls() { - const bubbleUiToggle = document.getElementById('enableUserChatBubbleUi'); - const metaSettings = document.getElementById('userChatBubbleMetaSettings'); - if (!bubbleUiToggle || !metaSettings) return; - metaSettings.style.display = bubbleUiToggle.checked ? 'flex' : 'none'; -} - -function applyUserChatBubbleUiState(settings = globalSettings) { - const applyLayoutState = window.domBuilder?.applyUserMessageLayoutState; - if (typeof applyLayoutState !== 'function') return; - - document.querySelectorAll('.message-item.user').forEach((messageItem) => { - applyLayoutState(messageItem, settings); - }); -} - -function applyChatBubbleLayoutSettings(settings = globalSettings) { - const rootStyle = document.documentElement.style; - const resolvedSettings = settings || {}; - const chatFontFamily = resolveChatFontFamily(resolvedSettings); - const chatCodeFontFamily = resolveChatCodeFontFamily(resolvedSettings); - const diaryFontFamily = resolveDiaryFontFamily(resolvedSettings); - const toolFontFamily = resolveToolFontFamily(resolvedSettings); - - const defaultWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthDefault, 82); - const notificationsWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthNotifications, 90); - const narrowWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthNarrow, 85); - const wideDefaultWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthWideDefault, 92); - const wideNotificationsWidth = clampChatBubbleWidthPercent(resolvedSettings.chatBubbleMaxWidthWideNotifications, 96); - const wideNarrowWidth = clampChatBubbleWidthPercent( - resolvedSettings.chatBubbleMaxWidthWideNarrow, - wideDefaultWidth - ); - - rootStyle.setProperty('--chat-bubble-max-width', `${defaultWidth}%`); - rootStyle.setProperty('--chat-bubble-max-width-notifications', `${notificationsWidth}%`); - rootStyle.setProperty('--chat-bubble-max-width-narrow', `${narrowWidth}%`); - rootStyle.setProperty('--chat-bubble-max-width-wide', `${wideDefaultWidth}%`); - rootStyle.setProperty('--chat-bubble-max-width-wide-notifications', `${wideNotificationsWidth}%`); - rootStyle.setProperty('--chat-bubble-max-width-wide-narrow', `${wideNarrowWidth}%`); - rootStyle.setProperty('--vcp-chat-font-family', chatFontFamily); - rootStyle.setProperty('--vcp-chat-code-font-family', chatCodeFontFamily); - rootStyle.setProperty('--vcp-diary-font-family', diaryFontFamily); - rootStyle.setProperty('--vcp-tool-card-font-family', toolFontFamily); - rootStyle.setProperty('--font-family', chatFontFamily); - rootStyle.setProperty('--font-family-sans-serif', chatFontFamily); - rootStyle.setProperty('--font-family-monospace', chatCodeFontFamily); - - if (window.pretextBridge && typeof window.pretextBridge.setChatFonts === 'function') { - window.pretextBridge.setChatFonts(chatFontFamily, chatCodeFontFamily); - } - - if (document.body) { - document.body.classList.toggle('chat-wide-layout', resolvedSettings.enableWideChatLayout === true); - } - applyChatPresentationMode(resolvedSettings.chatPresentationMode, { - persist: false, - preserveScroll: false, - source: 'layout-settings' - }); - applyUserChatBubbleUiState(resolvedSettings); -} - -/** - * 🟢 将全局设置同步到 UI 元素(仅在模态框实例化后调用) - */ -async function syncGlobalSettingsToUI() { - const safeSet = (id, value, prop = 'value') => { - const el = document.getElementById(id); - if (el) el[prop] = value; - }; - const safeCheck = (id, checked) => { - const el = document.getElementById(id); - if (el) el.checked = !!checked; - }; - const syncRustDebugPanelVisibility = () => { - const rustDebugModeEl = document.getElementById('rustDebugMode'); - const rustDebugPanelEl = document.getElementById('rustDebugPanel'); - if (rustDebugPanelEl) { - rustDebugPanelEl.style.display = rustDebugModeEl?.checked ? 'block' : 'none'; - } - }; - const joinKeywords = (value) => Array.isArray(value) ? value.join('\n') : ''; - const shouldShowRustGuardRules = () => { - const useRust = document.getElementById('rustUseAssistant')?.checked === true; - return useRust; - }; - const syncRustGuardRulesVisibility = () => { - const container = document.getElementById('rustGuardRulesContainer'); - if (container) { - container.style.display = shouldShowRustGuardRules() ? 'block' : 'none'; - } - }; - - safeSet('userName', globalSettings.userName || '用户'); - - const borderColor = globalSettings.userAvatarBorderColor || '#3d5a80'; - safeSet('userAvatarBorderColor', borderColor); - safeSet('userAvatarBorderColorText', borderColor); - - const nameColor = globalSettings.userNameTextColor || '#ffffff'; - safeSet('userNameTextColor', nameColor); - safeSet('userNameTextColorText', nameColor); - - safeCheck('userUseThemeColorsInChat', globalSettings.userUseThemeColorsInChat); - - const completedUrl = window.settingsManager.completeVcpUrl(globalSettings.vcpServerUrl || ''); - safeSet('vcpServerUrl', completedUrl); - safeSet('vcpApiKey', globalSettings.vcpApiKey || ''); - safeSet('fileKey', globalSettings.fileKey || ''); - safeSet('vcpLogUrl', globalSettings.vcpLogUrl || ''); - safeSet('vcpLogKey', globalSettings.vcpLogKey || ''); - safeSet('topicSummaryModel', globalSettings.topicSummaryModel || ''); - safeSet('continueWritingPrompt', globalSettings.continueWritingPrompt || '请继续'); - safeSet('flowlockContinueDelay', globalSettings.flowlockContinueDelay ?? 5); - safeCheck('voiceModeLocal', (globalSettings.voiceMode || 'local') !== 'network'); - safeCheck('voiceModeNetwork', (globalSettings.voiceMode || 'local') === 'network'); - safeSet('speechRecognizerBrowserPath', globalSettings.speechRecognizerBrowserPath || ''); - safeSet('speechRecognizerPagePath', globalSettings.speechRecognizerPagePath || 'Voicechatmodules/recognizer.html'); - safeSet('voiceLocalSovitsUrl', globalSettings.voiceLocalSettings?.sovitsUrl || ''); - safeSet('voiceLocalSovitsKey', globalSettings.voiceLocalSettings?.sovitsKey || ''); - safeSet('voiceNetworkProviderUrl', globalSettings.voiceNetworkSettings?.providerUrl || ''); - safeSet('voiceNetworkProviderKey', globalSettings.voiceNetworkSettings?.providerKey || ''); - - // Network Notes Paths - const networkNotesPathsContainer = document.getElementById('networkNotesPathsContainer'); - if (networkNotesPathsContainer) { - networkNotesPathsContainer.innerHTML = ''; - const paths = Array.isArray(globalSettings.networkNotesPaths) ? globalSettings.networkNotesPaths : (globalSettings.networkNotesPath ? [globalSettings.networkNotesPath] : []); - if (paths.length === 0) { - uiHelperFunctions.addNetworkPathInput(''); - } else { - paths.forEach(path => uiHelperFunctions.addNetworkPathInput(path)); - } - } - - safeCheck('enableSmoothStreaming', globalSettings.enableSmoothStreaming === true); - safeCheck('showHomeVisualBrand', globalSettings.showHomeVisualBrand !== false); - safeCheck('showHomeVisualTagline', globalSettings.showHomeVisualTagline !== false); - safeSet('homeVisualTagline', globalSettings.homeVisualTagline || '语义级打穿 AI、UI/UX、APP 与人类想象力的边界'); - const appearance = window.VCPAppearance?.normalize(globalSettings.appearanceProfile, 'next'); - safeSet('appearanceDensity', appearance?.density || 'comfortable'); - safeSet('appearanceRadius', appearance?.radius || 'small'); - safeSet('appearanceTypography', appearance?.typography || 'system'); - safeSet('appearanceFontScale', appearance?.fontScale || 'normal'); - safeSet('appearanceContentWidth', appearance?.contentWidth || 'full'); - safeSet('appearanceSidebarRowHeight', appearance?.sidebarRowHeight ?? 46); - safeSet('appearanceSidebarRowHeightValue', `${appearance?.sidebarRowHeight ?? 46}px`); - safeSet('appearanceSidebarAvatarSize', appearance?.sidebarAvatarSize ?? 32); - safeSet('appearanceSidebarAvatarSizeValue', `${appearance?.sidebarAvatarSize ?? 32}px`); - safeSet('appearanceSidebarRadius', appearance?.sidebarRadius || 'tuned'); - safeCheck(`appearanceSidebarRadiusChoice-${appearance?.sidebarRadius || 'tuned'}`, true); - safeSet('appearanceCustomRadius', appearance?.customRadius ?? 10); - safeSet('appearanceCustomRadiusValue', `${appearance?.customRadius ?? 10}px`); - document.getElementById('appearanceSidebarAvatarSize')?.dispatchEvent(new Event('input', { bubbles: true })); - safeSet('appearanceSurface', appearance?.surface || 'translucent'); - safeSet('chatFontPreset', globalSettings.chatFontPreset || 'system'); - safeSet('chatFontCustom', globalSettings.chatFontCustom || ''); - safeSet('chatCodeFontPreset', globalSettings.chatCodeFontPreset || 'consolas'); - safeSet('chatCodeFontCustom', globalSettings.chatCodeFontCustom || ''); - safeSet('chatDiaryFontPreset', globalSettings.chatDiaryFontPreset || 'serif'); - safeSet('chatDiaryFontCustom', globalSettings.chatDiaryFontCustom || ''); - safeSet('chatToolFontPreset', globalSettings.chatToolFontPreset || 'system'); - safeSet('chatToolFontCustom', globalSettings.chatToolFontCustom || ''); - const presentationMode = normalizeChatPresentationMode(globalSettings.chatPresentationMode); - safeCheck(`chatPresentationMode${presentationMode[0].toUpperCase()}${presentationMode.slice(1)}`, true); - safeCheck('chatLayoutModeWide', globalSettings.enableWideChatLayout === true); - safeCheck('chatLayoutModeNormal', globalSettings.enableWideChatLayout !== true); - safeCheck('enableUserChatBubbleUi', globalSettings.enableUserChatBubbleUi !== false); - safeCheck('showUserMetaInChatBubbleUi', globalSettings.showUserMetaInChatBubbleUi !== false); - safeSet('chatBubbleMaxWidthWideDefault', clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideDefault, 92)); - safeSet('chatBubbleMaxWidthWideNotifications', clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideNotifications, 96)); - safeSet( - 'chatBubbleMaxWidthWideNarrow', - clampChatBubbleWidthPercent( - globalSettings.chatBubbleMaxWidthWideNarrow, - clampChatBubbleWidthPercent(globalSettings.chatBubbleMaxWidthWideDefault, 92) - ) - ); - safeSet('minChunkBufferSize', globalSettings.minChunkBufferSize ?? 16); - safeSet('smoothStreamIntervalMs', globalSettings.smoothStreamIntervalMs ?? 100); - syncChatFontControls(); - syncWideChatLayoutControls(); - syncUserChatBubbleControls(); - syncChatPresentationModeControls(presentationMode); - - const chatFontPresetSelect = document.getElementById('chatFontPreset'); - const chatFontCustomInput = document.getElementById('chatFontCustom'); - const chatCodeFontPresetSelect = document.getElementById('chatCodeFontPreset'); - const chatCodeFontCustomInput = document.getElementById('chatCodeFontCustom'); - const chatDiaryFontPresetSelect = document.getElementById('chatDiaryFontPreset'); - const chatDiaryFontCustomInput = document.getElementById('chatDiaryFontCustom'); - const chatToolFontPresetSelect = document.getElementById('chatToolFontPreset'); - const chatToolFontCustomInput = document.getElementById('chatToolFontCustom'); - const wideModeRadio = document.getElementById('chatLayoutModeWide'); - const normalModeRadio = document.getElementById('chatLayoutModeNormal'); - const userBubbleUiToggle = document.getElementById('enableUserChatBubbleUi'); - const presentationModeRadios = document.querySelectorAll('input[name="chatPresentationMode"]'); - if (chatFontPresetSelect && !chatFontPresetSelect.dataset.boundFontToggle) { - chatFontPresetSelect.addEventListener('change', syncChatFontControls); - chatFontPresetSelect.dataset.boundFontToggle = 'true'; - } - if (chatCodeFontPresetSelect && !chatCodeFontPresetSelect.dataset.boundFontToggle) { - chatCodeFontPresetSelect.addEventListener('change', syncChatFontControls); - chatCodeFontPresetSelect.dataset.boundFontToggle = 'true'; - } - if (chatDiaryFontPresetSelect && !chatDiaryFontPresetSelect.dataset.boundFontToggle) { - chatDiaryFontPresetSelect.addEventListener('change', syncChatFontControls); - chatDiaryFontPresetSelect.dataset.boundFontToggle = 'true'; - } - if (chatToolFontPresetSelect && !chatToolFontPresetSelect.dataset.boundFontToggle) { - chatToolFontPresetSelect.addEventListener('change', syncChatFontControls); - chatToolFontPresetSelect.dataset.boundFontToggle = 'true'; - } - if (chatFontCustomInput && !chatFontCustomInput.dataset.boundFontPreview) { - chatFontCustomInput.addEventListener('input', updateFontScenarioPreview); - chatFontCustomInput.dataset.boundFontPreview = 'true'; - } - if (chatCodeFontCustomInput && !chatCodeFontCustomInput.dataset.boundFontPreview) { - chatCodeFontCustomInput.addEventListener('input', updateFontScenarioPreview); - chatCodeFontCustomInput.dataset.boundFontPreview = 'true'; - } - if (chatDiaryFontCustomInput && !chatDiaryFontCustomInput.dataset.boundFontPreview) { - chatDiaryFontCustomInput.addEventListener('input', updateFontScenarioPreview); - chatDiaryFontCustomInput.dataset.boundFontPreview = 'true'; - } - if (chatToolFontCustomInput && !chatToolFontCustomInput.dataset.boundFontPreview) { - chatToolFontCustomInput.addEventListener('input', updateFontScenarioPreview); - chatToolFontCustomInput.dataset.boundFontPreview = 'true'; - } - if (wideModeRadio && !wideModeRadio.dataset.boundWideChatToggle) { - wideModeRadio.addEventListener('change', syncWideChatLayoutControls); - wideModeRadio.dataset.boundWideChatToggle = 'true'; - } - if (normalModeRadio && !normalModeRadio.dataset.boundWideChatToggle) { - normalModeRadio.addEventListener('change', syncWideChatLayoutControls); - normalModeRadio.dataset.boundWideChatToggle = 'true'; - } - if (userBubbleUiToggle && !userBubbleUiToggle.dataset.boundUserBubbleToggle) { - userBubbleUiToggle.addEventListener('change', syncUserChatBubbleControls); - userBubbleUiToggle.dataset.boundUserBubbleToggle = 'true'; - } - presentationModeRadios.forEach((radio) => { - if (radio.dataset.boundPresentationModeToggle) return; - radio.addEventListener('change', () => { - if (!radio.checked) return; - applyChatPresentationMode(radio.value, { - persist: true, - preserveScroll: true, - notify: false, - source: 'settings' - }); - }); - radio.dataset.boundPresentationModeToggle = 'true'; - }); - - // User Avatar Preview - const userAvatarPreview = document.getElementById('userAvatarPreview'); - const userAvatarWrapper = userAvatarPreview?.closest('.agent-avatar-wrapper'); - if (userAvatarPreview) { - if (globalSettings.userAvatarUrl) { - userAvatarPreview.src = globalSettings.userAvatarUrl; - userAvatarPreview.style.display = 'block'; - userAvatarWrapper?.classList.remove('no-avatar'); - } else { - userAvatarPreview.src = '#'; - userAvatarPreview.style.display = 'none'; - userAvatarWrapper?.classList.add('no-avatar'); - } - } - - // 加载论坛配置并填充管理员账号/密码 - try { - const forumConfig = await chatAPI.loadForumConfig(); - if (forumConfig && !forumConfig.error) { - safeSet('adminUsername', forumConfig.username || ''); - safeSet('adminPassword', forumConfig.password || ''); - } - } catch (err) { - console.warn('[Renderer] Failed to load forum config for global settings:', err); - } - - // Assistant Select - const assistantAgentSelect = document.getElementById('assistantAgent'); - if (assistantAgentSelect) { - await window.settingsManager.populateAssistantAgentSelect(); - assistantAgentSelect.value = globalSettings.assistantAgent || ''; - } - - safeCheck('enableDistributedServer', globalSettings.enableDistributedServer === true); - safeCheck('agentMusicControl', globalSettings.agentMusicControl === true); - safeCheck('enableVcpToolInjection', globalSettings.enableVcpToolInjection === true); - safeCheck('enableThoughtChainInjection', globalSettings.enableThoughtChainInjection === true); - safeCheck('enableContextSanitizer', globalSettings.enableContextSanitizer === true); - safeSet('contextSanitizerDepth', globalSettings.contextSanitizerDepth ?? 2); - - const contextSanitizerDepthContainer = document.getElementById('contextSanitizerDepthContainer'); - if (contextSanitizerDepthContainer) { - contextSanitizerDepthContainer.style.display = globalSettings.enableContextSanitizer === true ? 'block' : 'none'; - } +}); - safeCheck('enableAiMessageButtons', globalSettings.enableAiMessageButtons !== false); - safeCheck('enableMiddleClickQuickAction', globalSettings.enableMiddleClickQuickAction === true); - safeSet('middleClickQuickAction', globalSettings.middleClickQuickAction || ''); - safeCheck('enableMiddleClickAdvanced', globalSettings.enableMiddleClickAdvanced === true); - safeSet('middleClickAdvancedDelay', Math.max(1000, globalSettings.middleClickAdvancedDelay ?? 1000)); - safeCheck('enableRegenerateConfirmation', globalSettings.enableRegenerateConfirmation !== false); +function loadAndApplyGlobalSettings() { return mainChatSettingsPresentationOwner.loadAndApply(); } - if (chatAPI?.getRustAssistantConfig) { - try { - const rustConfig = await chatAPI.getRustAssistantConfig(); - if (rustConfig && !rustConfig.error) { - safeCheck('rustUseAssistant', rustConfig.useRustAssistant === true); - safeCheck('rustDebugMode', rustConfig.debugMode === true); - safeSet('rustWhitelistKeywords', joinKeywords(rustConfig.whitelist || [])); - safeSet('rustBlacklistKeywords', joinKeywords(rustConfig.blacklist || [])); - safeSet('rustScreenshotApps', joinKeywords(rustConfig.screenshotApps || [])); - syncRustDebugPanelVisibility(); - syncRustGuardRulesVisibility(); - - const rustDebugModeEl = document.getElementById('rustDebugMode'); - if (rustDebugModeEl && !rustDebugModeEl.dataset.debugPanelBound) { - rustDebugModeEl.addEventListener('change', syncRustDebugPanelVisibility); - rustDebugModeEl.dataset.debugPanelBound = 'true'; - } - const rustUseAssistantEl = document.getElementById('rustUseAssistant'); - if (rustUseAssistantEl && !rustUseAssistantEl.dataset.guardPanelBound) { - rustUseAssistantEl.addEventListener('change', syncRustGuardRulesVisibility); - rustUseAssistantEl.dataset.guardPanelBound = 'true'; - } +function applyInitialThemeClass(mode) { mainChatThemeOwner.applyInitialTheme(mode); } - } - } catch (error) { - console.warn('[Renderer] Failed to sync Rust assistant config:', error); - } - } - if (chatAPI?.getAssistantRuntimeStatus && document.getElementById('rustDebugMode')?.checked) { - try { - const runtime = await chatAPI.getAssistantRuntimeStatus(); - if (runtime && runtime.success) { - const modeText = runtime.mode === 'rust' - ? 'Rust' - : (runtime.mode === 'disabled' ? 'Disabled' : runtime.mode || 'Unknown'); - const desiredText = runtime.desiredMode === 'rust' - ? 'Rust' - : (runtime.desiredMode === 'disabled' ? 'Disabled' : runtime.desiredMode || 'Unknown'); - const activeText = runtime.active ? '运行中' : '未运行'; - const debugReasonText = runtime.lastDebugReason || '无'; - const forwardedCount = runtime.forwardedEventCount || 0; - const sidecarActiveText = runtime.rustSidecarListenerActive === null - ? '未知' - : (runtime.rustSidecarListenerActive ? '是' : '否'); - const processAliveText = runtime.adapterProcessAlive ? '运行中' : '未运行'; - const processPidText = runtime.adapterProcessPid ? String(runtime.adapterProcessPid) : '无'; - const autoFallbackCount = runtime.runtimeFallbackTrace?.autoFallbackCount || 0; - const autoFallbackReason = runtime.runtimeFallbackTrace?.lastAutoFallbackReason || '无'; - const receivedCount = runtime.integrationTrace?.receivedSelectionCount || 0; - const showAttemptCount = runtime.integrationTrace?.showAttemptCount || 0; - const showErrorText = runtime.integrationTrace?.lastShowError || '无'; - safeSet('assistantRuntimeMode', modeText, 'textContent'); - safeSet('assistantRuntimeDesiredMode', desiredText, 'textContent'); - safeSet('assistantRuntimeActive', activeText, 'textContent'); - safeSet('assistantRuntimeDebugReason', debugReasonText, 'textContent'); - safeSet('assistantRuntimeForwardedCount', String(forwardedCount), 'textContent'); - safeSet('assistantRuntimeSidecarActive', sidecarActiveText, 'textContent'); - safeSet('assistantRuntimeProcessAlive', processAliveText, 'textContent'); - safeSet('assistantRuntimeProcessPid', processPidText, 'textContent'); - safeSet('assistantRuntimeAutoFallbackCount', String(autoFallbackCount), 'textContent'); - safeSet('assistantRuntimeAutoFallbackReason', autoFallbackReason, 'textContent'); - safeSet('assistantRuntimeReceivedCount', String(receivedCount), 'textContent'); - safeSet('assistantRuntimeShowAttemptCount', String(showAttemptCount), 'textContent'); - safeSet('assistantRuntimeShowError', showErrorText, 'textContent'); - } - } catch (error) { - console.warn('[Renderer] Failed to load assistant runtime status:', error); - } - } - - // Visibility toggles - const middleClickContainer = document.getElementById('middleClickQuickActionContainer'); - if (middleClickContainer) middleClickContainer.style.display = globalSettings.enableMiddleClickQuickAction ? 'block' : 'none'; - const middleClickAdvancedContainer = document.getElementById('middleClickAdvancedContainer'); - if (middleClickAdvancedContainer) middleClickAdvancedContainer.style.display = globalSettings.enableMiddleClickQuickAction ? 'block' : 'none'; - const middleClickAdvancedSettings = document.getElementById('middleClickAdvancedSettings'); - if (middleClickAdvancedSettings) middleClickAdvancedSettings.style.display = globalSettings.enableMiddleClickAdvanced ? 'block' : 'none'; -} +function normalizeChatPresentationMode(mode) { return mainChatSettingsPresentationOwner.normalizePresentation(mode); } +function applyChatPresentationMode(mode, options = {}) { return mainChatSettingsPresentationOwner.applyPresentation(mode, options); } +function setupChatPresentationQuickSwitcher() { return mainChatSettingsPresentationOwner.setupPresentationQuickSwitcher(); } +function applyChatBubbleLayoutSettings(settings = getGlobalSettings()) { return mainChatSettingsPresentationOwner.applyLayoutSettings(settings); } +function syncGlobalSettingsToUI() { return mainChatSettingsPresentationOwner.syncSettingsToUI(); } // --- Chat Functionality --- // --- UI Event Listeners & Helpers --- @@ -2546,179 +1197,7 @@ if (window.marked && typeof window.marked.Marked === 'function') { // Ensure Mar markedInstance = { parse: (text) => `

    ${String(text || '').replace(/\n/g, '
    ')}

    ` }; } -window.addEventListener('contextmenu', (e) => { - // Allow context menu for text input fields - if (e.target.closest('textarea, input[type="text"], .message-item .md-content')) { // Also allow on rendered message content - // Standard context menu will appear - } else { - // e.preventDefault(); // Optionally prevent context menu elsewhere - } -}, false); - // Helper to get a centrally stored cropped file (agent, group, or user) // These functions are now part of modules/ui-helpers.js and are accessed via uiHelperFunctions -// --- Forward Message Functionality --- -let messageToForward = null; -let selectedForwardTarget = null; - -async function showForwardModal(message) { - messageToForward = message; - selectedForwardTarget = null; // Reset selection - - // 🟢 修复:先调用 openModal 确保从模板实例化 DOM 元素 - uiHelperFunctions.openModal('forwardMessageModal'); - - const modal = document.getElementById('forwardMessageModal'); - const targetList = document.getElementById('forwardTargetList'); - const searchInput = document.getElementById('forwardTargetSearch'); - const commentInput = document.getElementById('forwardAdditionalComment'); - const confirmBtn = document.getElementById('confirmForwardBtn'); - - if (!targetList || !searchInput || !commentInput || !confirmBtn) { - console.error("[Forward Modal] Elements not found even after modal open!"); - return; - } - - targetList.innerHTML = '
  • Loading...
  • '; - commentInput.value = ''; - searchInput.value = ''; - confirmBtn.disabled = true; - - const result = await chatAPI.getAllItems(); - if (result.success) { - renderForwardTargetList(result.items); - } else { - targetList.innerHTML = '
  • Failed to load targets.
  • '; - } - - searchInput.oninput = () => { - const searchTerm = searchInput.value.toLowerCase(); - const items = targetList.querySelectorAll('.agent-item'); - items.forEach(item => { - const name = item.dataset.name.toLowerCase(); - if (name.includes(searchTerm)) { - item.style.display = ''; - } else { - item.style.display = 'none'; - } - }); - }; - - confirmBtn.onclick = handleConfirmForward; -} - -function renderForwardTargetList(items) { - const targetList = document.getElementById('forwardTargetList'); - const confirmBtn = document.getElementById('confirmForwardBtn'); - targetList.innerHTML = ''; - - items.forEach(item => { - const li = document.createElement('li'); - li.className = 'agent-item'; - li.dataset.id = item.id; - li.dataset.type = item.type; - li.dataset.name = item.name; - - const avatar = document.createElement('img'); - avatar.className = 'avatar'; - avatar.src = item.avatarUrl || (item.type === 'group' ? 'assets/default_group_avatar.png' : 'assets/default_user_avatar.png'); - - const nameSpan = document.createElement('span'); - nameSpan.className = 'agent-name'; - nameSpan.textContent = `${item.name} (${item.type === 'group' ? '群组' : 'Agent'})`; - - li.appendChild(avatar); - li.appendChild(nameSpan); - - li.onclick = () => { - const currentSelected = targetList.querySelector('.selected'); - if (currentSelected) { - currentSelected.classList.remove('selected'); - } - li.classList.add('selected'); - selectedForwardTarget = { id: item.id, type: item.type, name: item.name }; - confirmBtn.disabled = false; - }; - targetList.appendChild(li); - }); -} - -async function handleConfirmForward() { - if (!messageToForward || !selectedForwardTarget) { - uiHelperFunctions.showToastNotification('错误:未选择消息或转发目标。', 'error'); - return; - } - - const additionalComment = document.getElementById('forwardAdditionalComment').value.trim(); - - // We need to get the original message from history to ensure we have all data - const originalMessageResult = await chatAPI.getOriginalMessageContent( - currentSelectedItem.id, - currentSelectedItem.type, - currentTopicId, - messageToForward.id - ); - - if (!originalMessageResult.success) { - uiHelperFunctions.showToastNotification(`无法获取原始消息内容: ${originalMessageResult.error}`, 'error'); - return; - } - - const originalMessage = { ...messageToForward, content: originalMessageResult.content }; - - let forwardedContent = ''; - const senderName = originalMessage.name || (originalMessage.role === 'user' ? '用户' : '助手'); - forwardedContent += `> 转发自 **${senderName}** 的消息:\n\n`; - - let originalText = ''; - if (typeof originalMessage.content === 'string') { - originalText = originalMessage.content; - } else if (originalMessage.content && typeof originalMessage.content.text === 'string') { - originalText = originalMessage.content.text; - } - - forwardedContent += originalText; - - if (additionalComment) { - forwardedContent += `\n\n---\n${additionalComment}`; - } - - const attachments = originalMessage.attachments || []; - - // This is a simplified send. We might need a more robust solution - // that re-uses the logic from chatManager.handleSendMessage - // For now, let's create a new function in chatManager for this. - if (window.chatManager && typeof window.chatManager.handleForwardMessage === 'function') { - window.chatManager.handleForwardMessage(selectedForwardTarget, forwardedContent, attachments); - uiHelperFunctions.showToastNotification(`消息已转发给 ${selectedForwardTarget.name}`, 'success'); - } else { - uiHelperFunctions.showToastNotification('转发功能尚未完全实现。', 'error'); - console.error('chatManager.handleForwardMessage is not defined'); - } - - uiHelperFunctions.closeModal('forwardMessageModal'); - messageToForward = null; - selectedForwardTarget = null; -} -// Expose these functions globally for ui-helpers.js -// Expose the new helper functions on the window object for modules that need them -// These are no longer needed as uiHelperFunctions handles them directly -window.ensureAudioContext = () => { /* Placeholder, will be defined in setupTtsListeners */ }; -window.showForwardModal = showForwardModal; - -// Make globalSettings accessible for notification renderer -window.applyChatBubbleLayoutSettings = applyChatBubbleLayoutSettings; -window.applyChatPresentationMode = applyChatPresentationMode; -window.setChatPresentationMode = applyChatPresentationMode; -window.normalizeChatPresentationMode = normalizeChatPresentationMode; -window.globalSettings = globalSettings; - -// Make filter functions globally accessible for notification renderer -window.checkMessageFilter = (messageTitle) => { - if (window.filterManager) { - return window.filterManager.checkMessageFilter(messageTitle); - } - // Fallback if the manager is not available - return null; -}; +// Forward modal ownership is provided by ForwardMessageOwner. diff --git a/rust_assistant_engine/ui/assistant.html b/rust_assistant_engine/ui/assistant.html index dda5cf1c7..83b89e1a7 100644 --- a/rust_assistant_engine/ui/assistant.html +++ b/rust_assistant_engine/ui/assistant.html @@ -53,7 +53,6 @@ - - + diff --git a/rust_assistant_engine/ui/assistant.js b/rust_assistant_engine/ui/assistant.js index 2093c86d6..629ce9d3d 100644 --- a/rust_assistant_engine/ui/assistant.js +++ b/rust_assistant_engine/ui/assistant.js @@ -1,4 +1,15 @@ -// Assistantmodules/assistant.js +// Assistantmodules/assistant.js +import { createMemoryChatRepository } from '../../modules/chat/memoryChatRepository.js'; +import { createTransientChatHistoryPersistence } from '../../modules/chat/chatHistoryPersistence.js'; +import { createChatHistoryMutationAuthority } from '../../modules/chat/chatHistoryMutationAuthority.js'; +import { createChatRepository } from '../../modules/chat/chatRepository.js'; +import { createWindowStreamRuntime } from '../../modules/renderer/windowStreamRuntime.js'; +import { createMessageRenderer } from '../../modules/messageRenderer.js'; +import { createStreamProjection } from '../../modules/renderer/streamManager.js'; +import { createStreamTransientHistory } from '../../modules/chat/streamTransientHistory.js'; + +const streamManager = createStreamProjection(); +const messageRenderer = createMessageRenderer({ streamManager }); document.addEventListener('DOMContentLoaded', () => { const chatMessagesDiv = document.getElementById('chatMessages'); @@ -15,7 +26,9 @@ document.addEventListener('DOMContentLoaded', () => { let globalSettings = {}; let currentChatHistory = []; let attachedFiles = []; - let activeStreamingMessageId = null; + let activeStreamingMessageId = null; + let streamRuntime = null; + let historyMutationAuthority = null; const markedInstance = new window.marked.Marked({ gfm: true, breaks: true }); const scrollToBottom = () => { @@ -37,13 +50,22 @@ document.addEventListener('DOMContentLoaded', () => { document.body.style.pointerEvents = 'none'; } - try { - await saveAssistantChatToHistory(); - } catch (error) { - console.error('[Assistant] Error saving history on close:', error); - } finally { - window.close(); - } + try { + if (activeStreamingMessageId) { + await window.electronAPI.interruptVcpRequest?.({ messageId: activeStreamingMessageId }); + await streamRuntime?.cancel(activeStreamingMessageId, 'assistant-window-close'); + } + await saveAssistantChatToHistory(); + } catch (error) { + console.error('[Assistant] Error saving history on close:', error); + } finally { + await streamRuntime?.dispose(); + await streamManager.dispose(); + await messageRenderer.disposeRootResources(chatMessagesDiv); + messageRenderer.disposeRendererResources(); + await historyMutationAuthority?.dispose(); + window.close(); + } }); function getAssistantTopicId() { @@ -60,44 +82,18 @@ document.addEventListener('DOMContentLoaded', () => { return eventTopicId === getAssistantTopicId() && eventAgentId === agentId; } - async function waitForActiveStreamToSettle(timeoutMs = 4000) { - if (!activeStreamingMessageId) return true; - - const pendingMessageId = activeStreamingMessageId; - console.log(`[Assistant] Waiting for active stream to settle before close: ${pendingMessageId}`); - - return await new Promise((resolve) => { - let settled = false; - const startedAt = Date.now(); - - const check = () => { - if (settled) return; - if (activeStreamingMessageId !== pendingMessageId) { - settled = true; - resolve(true); - return; - } - - if (Date.now() - startedAt >= timeoutMs) { - console.warn(`[Assistant] Timed out while waiting stream to settle: ${pendingMessageId}`); - settled = true; - resolve(false); - return; - } - - setTimeout(check, 80); - }; - - check(); - }); - } + function restoreComposerAfterStream() { + activeStreamingMessageId = null; + messageInput.disabled = false; + sendMessageBtn.disabled = false; + if (attachFileBtn) attachFileBtn.disabled = false; + messageInput.focus(); + } async function saveAssistantChatToHistory() { if (!agentId) return; - await waitForActiveStreamToSettle(); - - const persistedHistory = currentChatHistory.filter(msg => !msg.isThinking && msg.role !== 'system'); + const persistedHistory = currentChatHistory.filter(msg => !msg.isThinking && msg.role !== 'system'); if (persistedHistory.length === 0) return; console.log('[Assistant] Saving chat history before exit...'); @@ -109,7 +105,8 @@ document.addEventListener('DOMContentLoaded', () => { if (result && result.success && result.topicId) { const newTopicId = result.topicId; - await window.electronAPI.saveChatHistory(agentId, newTopicId, persistedHistory); + if (!historyMutationAuthority) throw new Error('Assistant history mutation authority is not ready'); + await historyMutationAuthority.replace({ itemId: agentId, itemType: 'agent', topicId: newTopicId, category: 'assistant-session-close' }, persistedHistory); console.log(`[Assistant] History saved to new topic: ${newTopicId}`); if (window.summarizeTopicFromMessages) { @@ -175,7 +172,7 @@ window.electronAPI.onAssistantData(async (data) => { agentNameSpan.textContent = agentConfig.name; // --- Initialize Shared Renderer --- - if (window.messageRenderer) { + if (messageRenderer) { const chatHistoryRef = { get: () => currentChatHistory, set: (newHistory) => { currentChatHistory = newHistory; } @@ -204,7 +201,7 @@ window.electronAPI.onAssistantData(async (data) => { if (activeStreamingMessageId === messageId) { // Notify the main process to stop the VCP request. // The main process should then send an 'end' or 'error' stream event, - // which will trigger finalizeStreamedMessage correctly. + // which will publish the coordinator-owned terminal outcome. await window.electronAPI.interruptVcpRequest({ messageId }); return { success: true }; } @@ -212,22 +209,54 @@ window.electronAPI.onAssistantData(async (data) => { } }; - window.messageRenderer.initializeMessageRenderer({ - currentChatHistoryRef: chatHistoryRef, + const chatRepository = createMemoryChatRepository({ + read: () => currentChatHistory, + write: history => { currentChatHistory = history; }, + }); + const historyPersistence = createTransientChatHistoryPersistence(chatRepository); + const transientStreamHistory = createStreamTransientHistory({ + repository: chatRepository, + currentHistory: { get: () => chatHistoryRef.get(), replace: history => chatHistoryRef.set(history) }, + }); + historyMutationAuthority = createChatHistoryMutationAuthority({ repository: createChatRepository(window.electronAPI) }); + messageRenderer.initializeMessageRenderer({ + chatRepository, + historyMutationAuthority, + currentChatHistoryRef: chatHistoryRef, currentSelectedItemRef: selectedItemRef, - currentTopicIdRef: topicIdRef, + currentTopicIdRef: topicIdRef, + transientStreamHistory, + viewAuthority: { isCurrent: context => context?.agentId === agentId && context?.topicId === getAssistantTopicId() }, globalSettingsRef: globalSettingsRef, chatMessagesDiv: chatMessagesDiv, - electronAPI: window.electronAPI, - markedInstance: markedInstance, - uiHelper: window.uiHelperFunctions, - summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => ""), + electronAPI: window.electronAPI, + markedInstance: markedInstance, + morphdom: window.morphdom, + pretextBridge: window.pretextBridge, + flowlockProtocol: window.flowlockProtocol, + uiHelper: window.uiHelperFunctions, + messageCommands: { handleSendMessage: text => sendMessage(text) }, + summarizeTopicFromMessages: window.summarizeTopicFromMessages || (async () => ""), handleCreateBranch: () => {}, // Stub interruptHandler: interruptHandler // Provide the interrupt handler - }); - console.log('[Assistant] Shared messageRenderer initialized.'); + }); + streamRuntime = createWindowStreamRuntime({ + root: chatMessagesDiv, + streamProjection: streamManager, + historyPersistence, + getSelection: () => ({ id: agentId, type: 'agent' }), + getTopicId: getAssistantTopicId, + getMessageContext: () => ({ + agentId, topicId: getAssistantTopicId(), agentName: agentConfig?.name, + avatarUrl: agentConfig?.avatarUrl, avatarColor: agentConfig?.avatarCalculatedColor, + }), + contextFilter: context => !!context && context.topicId === getAssistantTopicId() && context.agentId === agentId, + dispatchTerminal: detail => window.dispatchEvent(new CustomEvent('vcp-chat-stream-terminal', { detail })), + onSettled: restoreComposerAfterStream, + }); + console.log('[Assistant] Shared messageRenderer initialized.'); } else { - console.error('[Assistant] window.messageRenderer is not available. Cannot initialize shared renderer.'); + console.error('[Assistant] shared message renderer provider is not available. Cannot initialize shared renderer.'); agentNameSpan.textContent = "错误"; chatMessagesDiv.innerHTML = `

    加载渲染模块失败,请重启应用。

    `; return; @@ -296,7 +325,7 @@ window.electronAPI.onAssistantData(async (data) => { const sendMessage = async (messageContent) => { if (!messageContent.trim() && attachedFiles.length === 0) return; - if (!agentConfig || !window.messageRenderer) return; + if (!agentConfig || !messageRenderer) return; const uiAttachments = []; if (attachedFiles.length > 0) { @@ -319,7 +348,7 @@ window.electronAPI.onAssistantData(async (data) => { id: `user_msg_${Date.now()}`, attachments: uiAttachments }; - await window.messageRenderer.renderMessage(userMessage, false); + await messageRenderer.renderMessage(userMessage, false); currentChatHistory.push(userMessage); messageInput.value = ''; @@ -342,7 +371,7 @@ window.electronAPI.onAssistantData(async (data) => { name: agentConfig.name, avatarUrl: agentConfig.avatarUrl }; - await window.messageRenderer.renderMessage(assistantMessagePlaceholder, false); + await messageRenderer.renderMessage(assistantMessagePlaceholder, false); // Context is required for the new sendToVCP API const context = { @@ -415,67 +444,21 @@ window.electronAPI.onAssistantData(async (data) => { // Call with new signature, including context. isGroupCall is false. await window.electronAPI.sendToVCP(globalSettings.vcpServerUrl, globalSettings.vcpApiKey, messagesForVCP, modelConfig, thinkingMessageId, false, context); - } catch (error) { - console.error('Error sending message to VCP:', error); - if (window.messageRenderer) { - // Finalize without context to prevent history saving, then update UI - window.messageRenderer.finalizeStreamedMessage(thinkingMessageId, 'error'); - const messageItemContent = document.querySelector(`.message-item[data-message-id="${thinkingMessageId}"] .md-content`); - if (messageItemContent) { - messageItemContent.innerHTML = `

    请求失败: ${error.message}

    `; - } - } - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - if (attachFileBtn) attachFileBtn.disabled = false; - messageInput.focus(); - } + } catch (error) { + console.error('Error sending message to VCP:', error); + const accepted = streamRuntime?.accept({ + type: 'error', messageId: thinkingMessageId, error: error.message, + context: { agentId, topicId: getAssistantTopicId() }, + }); + if (!accepted) restoreComposerAfterStream(); + } }; - const activeStreams = new Set(); - // Listen to the new, unified stream event - window.electronAPI.onVCPStreamEvent((eventData) => { - if (!window.messageRenderer || !isEventForCurrentAssistantSession(eventData)) return; - - const { messageId, type, chunk, error, context } = eventData; - - // The 'start' event is implicit. The first 'data' chunk will trigger startStreamingMessage. - if (!activeStreams.has(messageId) && type === 'data') { - window.messageRenderer.startStreamingMessage({ - id: messageId, - role: 'assistant', - name: agentConfig.name, - avatarUrl: agentConfig.avatarUrl, - context: context, // Pass context - }); - activeStreams.add(messageId); - } - - if (type === 'data') { - window.messageRenderer.appendStreamChunk(messageId, chunk, context); - } else if (type === 'end') { - window.messageRenderer.finalizeStreamedMessage(messageId, 'completed', context); - activeStreams.delete(messageId); - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - if (attachFileBtn) attachFileBtn.disabled = false; - messageInput.focus(); - } else if (type === 'error') { - window.messageRenderer.finalizeStreamedMessage(messageId, 'error', context); - const messageItemContent = document.querySelector(`.message-item[data-message-id="${messageId}"] .md-content`); - if (messageItemContent) { - messageItemContent.innerHTML = `

    ${error || '未知流错误'}

    `; - } - activeStreams.delete(messageId); - activeStreamingMessageId = null; - messageInput.disabled = false; - sendMessageBtn.disabled = false; - if (attachFileBtn) attachFileBtn.disabled = false; - messageInput.focus(); - } - }); + // Listen to the new, unified stream event + window.electronAPI.onVCPStreamEvent((eventData) => { + if (!streamRuntime || !isEventForCurrentAssistantSession(eventData)) return; + streamRuntime.accept(eventData); + }); if (attachFileBtn) { attachFileBtn.addEventListener('click', async () => { diff --git a/scripts/build-chat-event-graph.mjs b/scripts/build-chat-event-graph.mjs new file mode 100644 index 000000000..5d91544f7 --- /dev/null +++ b/scripts/build-chat-event-graph.mjs @@ -0,0 +1,94 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const registeredContracts = JSON.parse(fs.readFileSync(path.join(root, 'docs/contracts/chat-contracts.json'), 'utf8')); +const dynamicRegistrations = registeredContracts.flatMap(contract => + (contract.dynamicSites || []).map(site => ({ ...site, contractId: contract.id })) +); +const sourceRoots = ['main.js', 'renderer.js', 'preloads', 'modules', 'Flowlockmodules', 'VCPDistributedServer']; +const ignored = /(?:^|[\\/])(?:tests?|node_modules|vendor|artifacts|docs)(?:[\\/]|$)/; +const files = []; + +function walk(relative) { + const absolute = path.join(root, relative); + if (!fs.existsSync(absolute)) return; + const stat = fs.statSync(absolute); + if (stat.isFile()) { if (/\.(?:js|mjs|cjs)$/.test(relative)) files.push(relative.replaceAll('\\', '/')); return; } + for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) { + const child = path.join(relative, entry.name).replaceAll('\\', '/'); + if (!ignored.test(child)) walk(child); + } +} +for (const sourceRoot of sourceRoots) walk(sourceRoot); + +const eventPatterns = [ + { kind: 'custom-event-dispatch', regex: /dispatchEvent\(new (?:CustomEvent|CustomEventConstructor)\(['"]([^'"]+)['"]/g }, + { kind: 'custom-event-listener', regex: /(?:addEventListener|on)\(['"]([^'"]+)['"]/g }, + { kind: 'ipc-send', regex: /(?:send|invoke|on|once)\(['"]([^'"]+)['"]/g }, + { kind: 'preload-subscription', regex: /(?:onVCPStreamEvent|onThemeUpdated|on[A-Z][A-Za-z]+)\s*\(/g }, + { kind: 'stream-terminal', regex: /(?:terminal|finish|complete)\s*\(\s*['"](completed|failed|cancelled|aborted|discarded|error)['"]/g } +]; +const nodes = new Map(); +const undiscovered = []; +const registeredDynamic = []; +const add = (name, role, file, line, kind, evidence) => { + if (!name || name.length < 2 || /^https?:/.test(name) || /\s/.test(name) || /[\u3400-\u9fff]/.test(name)) return; + if (!/(?:vcp|chat|stream|theme|history|topic|notification|appearance|flowlock|desktop|message|electron|plugin|surface|terminal|settings|assistant|voice|rust|push|cancel|retry|attachment|window|modal|conversation|selection|preload|ipc|^main\/|^agent\/|^session\/)/i.test(name) + && !name.includes('/')) return; + const key = name; + const node = nodes.get(key) || { name, producers: [], consumers: [], evidence: [] }; + const item = { file, line, kind }; + node[role].push(item); + node.evidence.push({ ...item, match: evidence }); + nodes.set(key, node); +}; + +for (const file of files) { + const source = fs.readFileSync(path.join(root, file), 'utf8'); + for (const pattern of eventPatterns) { + for (const match of source.matchAll(pattern.regex)) { + const offset = match.index ?? 0; + const line = source.slice(0, offset).split('\n').length; + const name = match[1] || match[0].replace(/\s+/g, ' ').slice(0, 120); + const isProducer = pattern.kind.includes('dispatch') || pattern.kind === 'ipc-send' || pattern.kind === 'stream-terminal'; + add(name, isProducer ? 'producers' : 'consumers', file, line, pattern.kind, match[0]); + } + } + for (const match of source.matchAll(/(?:emit|dispatch|send|invoke|on|once)\s*\(\s*eventName\b/g)) { + undiscovered.push({ file, line: source.slice(0, match.index).split('\n').length, reason: 'dynamic event name' }); + } + for (const match of source.matchAll(/new (?:CustomEvent|CustomEventConstructor)\(\s*(?!['"])([^,)\s]+)/g)) { + const line = source.slice(0, match.index).split('\n').length; + const registration = dynamicRegistrations.find(site => site.file === file && site.line === line); + const entry = { file, line, reason: `dynamic CustomEvent name: ${match[1]}` }; + if (registration) registeredDynamic.push({ ...entry, contractId: registration.contractId }); + else undiscovered.push(entry); + } +} + +const graph = { + schemaVersion: 1, + sourceRoots, + filesScanned: files.sort(), + events: [...nodes.values()].map(node => ({ + ...node, + producers: [...new Map(node.producers.map(item => [`${item.file}:${item.line}:${item.kind}`, item])).values()], + consumers: [...new Map(node.consumers.map(item => [`${item.file}:${item.line}:${item.kind}`, item])).values()] + })).sort((a, b) => a.name.localeCompare(b.name)), + registeredDynamic: [...new Map(registeredDynamic.map(item => [`${item.file}:${item.line}`, item])).values()], + undiscovered: [...new Map(undiscovered.map(item => [`${item.file}:${item.line}`, item])).values()] +}; +const output = path.join(root, 'docs/contracts/generated/chat-event-graph.json'); +const serialized = `${JSON.stringify(graph, null, 2)}\n`; +if (process.argv.includes('--check')) { + if (!fs.existsSync(output) || fs.readFileSync(output, 'utf8') !== serialized) { + console.error('Chat event graph is stale; run npm run build:chat-event-graph and review the diff.'); + process.exit(1); + } +} else { + fs.mkdirSync(path.dirname(output), { recursive: true }); + fs.writeFileSync(output, serialized, 'utf8'); + console.log(`Chat event graph generated (${graph.events.length} events, ${graph.filesScanned.length} files, ${graph.registeredDynamic.length} registered and ${graph.undiscovered.length} undiscovered dynamic sites).`); +} diff --git a/scripts/check-artifact-plane.mjs b/scripts/check-artifact-plane.mjs new file mode 100644 index 000000000..1d3b59032 --- /dev/null +++ b/scripts/check-artifact-plane.mjs @@ -0,0 +1,24 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fail = message => { throw new Error(`Artifact plane check failed: ${message}`); }; +const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +if (packageJson.main !== 'main.js' || !fs.existsSync(path.join(root, packageJson.main))) fail('package main entry is missing'); +if (typeof packageJson.scripts?.build !== 'string') fail('build script is missing'); +const electronPackage = path.join(root, 'node_modules', 'electron', 'package.json'); +if (!fs.existsSync(electronPackage)) fail('Electron package is not installed'); +const electron = await import('electron'); +const runtime = typeof electron.default === 'string' ? electron.default : electron; +if (typeof runtime !== 'string' || !fs.existsSync(runtime)) fail('Electron runtime binary is missing'); +for (const file of ['modules/chat/streamSession.js', 'modules/chat/streamCoordinator.js', 'renderer.js']) { + if (!fs.existsSync(path.join(root, file))) fail(`source entry missing: ${file}`); +} +const nativeRuntime = process.env.VCPCHAT_NATIVE_RUNTIME_INPUT + ? path.resolve(process.env.VCPCHAT_NATIVE_RUNTIME_INPUT) + : path.join(root, 'modules/services/chatDataService/bin', `${process.platform}-${process.arch}`, process.platform === 'win32' ? 'vcp_chat_data_service.exe' : 'vcp_chat_data_service'); +if (fs.existsSync(nativeRuntime)) console.log(`Native artifact present: ${nativeRuntime}`); +else if (process.env.VCPCHAT_REQUIRE_NATIVE_ARTIFACT === '1') fail(`required native artifact is missing: ${nativeRuntime}`); +else console.warn(`Native artifact not built in this source-plane run: ${nativeRuntime}`); +console.log(`Artifact plane check passed (Electron runtime: ${runtime}).`); diff --git a/scripts/check-chat-contracts.mjs b/scripts/check-chat-contracts.mjs new file mode 100644 index 000000000..5cb33ca5d --- /dev/null +++ b/scripts/check-chat-contracts.mjs @@ -0,0 +1,101 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fail = message => { console.error(`Chat contract gate failed: ${message}`); process.exitCode = 1; }; +const schema = JSON.parse(fs.readFileSync(path.join(root, 'docs/contracts/chat-contract.schema.json'), 'utf8')); +const contractsPath = process.env.VCPCHAT_CONTRACTS_INPUT + ? path.resolve(process.env.VCPCHAT_CONTRACTS_INPUT) + : path.join(root, 'docs/contracts/chat-contracts.json'); +const contracts = JSON.parse(fs.readFileSync(contractsPath, 'utf8')); +if (!Array.isArray(contracts)) fail('docs/contracts/chat-contracts.json must be an array'); +const ids = new Set(); +for (const contract of contracts) { + if (!contract || typeof contract !== 'object') { fail('contract entry must be an object'); continue; } + for (const required of schema.required) if (!(required in contract)) fail(`${contract.id || ''} missing ${required}`); + if (typeof contract.id !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(contract.id)) fail(`${contract.id || ''} has invalid id`); + if (typeof contract.owner !== 'string' || contract.owner.length === 0) fail(`${contract.id || ''} requires a non-empty owner`); + if (ids.has(contract.id)) fail(`duplicate contract id: ${contract.id}`); + ids.add(contract.id); + if (!schema.properties.kind.enum.includes(contract.kind)) fail(`${contract.id} has invalid kind`); + if (!schema.properties.status.enum.includes(contract.status)) fail(`${contract.id} has invalid status`); + if (contract.status === 'pass' && Array.isArray(contract.evidence) + && contract.evidence.some(item => /\b(skipped|manual_required|failed)\b/i.test(String(item)))) { + fail(`${contract.id} cannot declare pass with skipped/manual_required/failed evidence`); + } + if (contract.kind === 'event') { + if (!Array.isArray(contract.producer) || contract.producer.length === 0) fail(`${contract.id} requires a producer`); + if (!Array.isArray(contract.consumers) || contract.consumers.length === 0) fail(`${contract.id} requires a consumer`); + if (!contract.terminal || typeof contract.terminal !== 'object') fail(`${contract.id} requires terminal rules`); + } + if (contract.kind === 'facade') { + if (!Array.isArray(contract.consumers) || contract.consumers.length === 0) fail(`${contract.id} requires registered consumers`); + if (typeof contract.dynamicSmoke !== 'string' || !contract.dynamicSmoke) fail(`${contract.id} requires dynamic smoke`); + if (typeof contract.retirement !== 'string' || !contract.retirement) fail(`${contract.id} requires retirement decision`); + } + if (contract.kind === 'snapshot') { + if (typeof contract.snapshot !== 'string' || !contract.snapshot) fail(`${contract.id} requires a snapshot fixture`); + else if (!fs.existsSync(path.join(root, contract.snapshot))) fail(`${contract.id} snapshot fixture is missing: ${contract.snapshot}`); + if (typeof contract.sourceEntry !== 'string' || !fs.existsSync(path.join(root, contract.sourceEntry))) fail(`${contract.id} source entry is missing`); + } + if (contract.dynamicSites !== undefined) { + if (contract.kind !== 'event' || contract.dynamic !== true) fail(`${contract.id} dynamicSites requires a dynamic event contract`); + if (!Array.isArray(contract.dynamicSites) || contract.dynamicSites.length === 0) fail(`${contract.id} dynamicSites must not be empty`); + for (const site of contract.dynamicSites) { + if (!site || typeof site.file !== 'string' || !Number.isInteger(site.line) || site.line < 1) { + fail(`${contract.id} has an invalid dynamicSites entry`); + } else if (!fs.existsSync(path.join(root, site.file))) { + fail(`${contract.id} dynamicSites file is missing: ${site.file}`); + } + } + } +} +const generator = process.env.VCPCHAT_GRAPH_INPUT ? null : spawnSync(process.execPath, [path.join(root, 'scripts/build-chat-event-graph.mjs'), '--check'], { cwd: root, encoding: 'utf8' }); +if (generator && generator.status !== 0) { fail(generator.stderr || generator.stdout || 'graph generator failed'); } +const graphPath = process.env.VCPCHAT_GRAPH_INPUT + ? path.resolve(process.env.VCPCHAT_GRAPH_INPUT) + : path.join(root, 'docs/contracts/generated/chat-event-graph.json'); +if (!fs.existsSync(graphPath)) fail('generated chat-event graph is missing'); +const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8')); +if (graph.schemaVersion !== 1 || !Array.isArray(graph.events) || !Array.isArray(graph.registeredDynamic) || !Array.isArray(graph.undiscovered)) fail('generated graph has invalid shape'); +if (graph.undiscovered.length) fail(`graph contains ${graph.undiscovered.length} unregistered dynamic event site(s)`); +for (const contract of contracts.filter(item => item.kind === 'event' && item.dynamic === true)) { + for (const site of contract.dynamicSites || []) { + if (!graph.registeredDynamic.some(item => item.contractId === contract.id && item.file === site.file && item.line === site.line)) { + fail(`${contract.id} dynamicSites entry is not observed by the generated graph: ${site.file}:${site.line}`); + } + } +} +const consumerReportPath = process.env.VCPCHAT_CONSUMER_REPORT_INPUT + ? path.resolve(process.env.VCPCHAT_CONSUMER_REPORT_INPUT) + : path.join(root, 'docs/chat-kernel-consumer-report.json'); +if (fs.existsSync(consumerReportPath)) { + const consumerReport = JSON.parse(fs.readFileSync(consumerReportPath, 'utf8')); + const facades = consumerReport.ambientFacades || {}; + const facadeContracts = contracts.filter(contract => contract.kind === 'facade'); + for (const contract of facadeContracts) { + const name = contract.facadeName; + if (!name || !facades[name]) { fail(`${contract.id} must map to a consumer-report facadeName`); continue; } + const ledger = facades[name]; + if (ledger.classification !== 'supported-public-facade') fail(`${name} registry entry is not a supported public facade`); + if (ledger.definitions?.some(definition => definition.file === contract.owner) !== true) fail(`${name} registry owner does not match its definition`); + if (ledger.production.length === 0) fail(`${name} registry facade has no production evidence`); + } + for (const [name, evidence] of Object.entries(facades)) { + if (!evidence || typeof evidence !== 'object') fail(`facade ${name} has invalid ledger entry`); + if (evidence.classification === 'supported-public-facade') { + if (!Array.isArray(evidence.production) || evidence.production.length === 0) fail(`supported facade ${name} has no production consumer`); + if (!evidence.owner && (!Array.isArray(evidence.definitions) || evidence.definitions.length === 0)) fail(`supported facade ${name} has no owner/definition`); + } + } + const registeredFacadeNames = new Set(facadeContracts.map(contract => contract.facadeName).filter(Boolean)); + for (const [name, evidence] of Object.entries(facades)) { + if (evidence?.classification === 'supported-public-facade' && !registeredFacadeNames.has(name)) { + fail(`${name} is a supported public facade but has no registered contract`); + } + } +} +if (process.exitCode) process.exit(process.exitCode); +console.log(`Chat contract gate passed (${contracts.length} registered contracts, ${graph.events.length} generated events).`); diff --git a/scripts/check-chat-evidence.mjs b/scripts/check-chat-evidence.mjs new file mode 100644 index 000000000..0e6184a56 --- /dev/null +++ b/scripts/check-chat-evidence.mjs @@ -0,0 +1,24 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const commands = [ + ['scripts/check-chat-contracts.mjs'], + ['scripts/run-chat-contract-invariants.mjs'], + ['scripts/test-chat-contract-invalid.mjs'], + ['scripts/test-facade-registry-invalid.mjs'], + ['scripts/test-artifact-plane-invalid.mjs'], + ['scripts/test-packaged-artifact-invalid.mjs'], + ['scripts/check-artifact-plane.mjs'], + ['scripts/test-built-artifact-smoke.mjs'], + ['scripts/test-packaged-artifact-smoke.mjs'], + ['scripts/test-chat-transcript-snapshot.mjs'], + ['scripts/test-chat-evidence-manifest.mjs'], + ['scripts/test-chat-release-evidence-invalid.mjs'], +]; +for (const args of commands) { + const result = spawnSync(process.execPath, args, { cwd: root, stdio: 'inherit' }); + if (result.status !== 0) process.exit(result.status || 1); +} +console.log('Chat evidence gate passed (contracts, invariants, invalid runner, artifact plane and transcript snapshot).'); diff --git a/scripts/check-chat-kernel-consumers.mjs b/scripts/check-chat-kernel-consumers.mjs new file mode 100644 index 000000000..08aaa6475 --- /dev/null +++ b/scripts/check-chat-kernel-consumers.mjs @@ -0,0 +1,477 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const read = relative => fs.readFileSync(path.join(root, relative), 'utf8'); +const exists = relative => fs.existsSync(path.join(root, relative)); +const packageScripts = JSON.parse(read('package.json')).scripts || {}; + +const ignoredProductionDirectoryNames = new Set([ + '.git', + 'docs', + 'node_modules', + 'screenshots', + 'scripts', + 'tests', + 'vendor', +]); +const ignoredProductionDirectoryPaths = new Set(['audio_engine/AppData']); +const productionFiles = []; +const collectProductionFiles = (directory, relative = '') => { + if (!fs.existsSync(directory)) return; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const rel = path.join(relative, entry.name).replaceAll(path.sep, '/'); + if (entry.isDirectory()) { + if (!ignoredProductionDirectoryNames.has(entry.name) && !ignoredProductionDirectoryPaths.has(rel)) { + collectProductionFiles(path.join(directory, entry.name), rel); + } + } else if (/\.(?:js|mjs|html)$/.test(entry.name)) { + productionFiles.push(rel); + } + } +}; +collectProductionFiles(root); +productionFiles.sort(); +const testFiles = fs.readdirSync(path.join(root, 'tests')) + .filter(file => /chat|stream|main-chat|lifecycle/i.test(file)) + .map(file => `tests/${file}`); + +const source = file => read(file); +const count = (text, pattern) => [...text.matchAll(pattern)].length; +const providerFiles = Object.freeze({ + chatManager: 'modules/chatManager.js', + messageRenderer: 'modules/messageRenderer.js', + streamManager: 'modules/renderer/streamManager.js', +}); +const surfaceFor = file => { + if (file === 'renderer.js' || file.startsWith('modules/event-') || file.startsWith('modules/mainChat')) return 'main-window'; + if (file.startsWith('Voicechatmodules/')) return 'voice-chat'; + if (file.startsWith('rust_assistant_engine/')) return 'rust-assistant'; + if (file.startsWith('Flowlockmodules/')) return 'flowlock'; + if (file.startsWith('modules/ui-system/')) return 'internal-app'; + if (file.startsWith('tests/')) return 'test'; + return 'shared-renderer'; +}; +const withoutComments = text => { + let block = false; + return text.split(/\r?\n/).map(line => { + let output = ''; + for (let index = 0; index < line.length; index += 1) { + if (block) { + const end = line.indexOf('*/', index); + if (end < 0) return output; + block = false; + index = end + 1; + continue; + } + if (line.startsWith('//', index)) break; + if (line.startsWith('/*', index)) { + block = true; + index += 1; + continue; + } + output += line[index]; + } + return output; + }); +}; +const memberEvidence = (file, name) => { + if (file === providerFiles[name]) return []; + const rawLines = source(file).split(/\r?\n/); + const codeLines = withoutComments(source(file)); + const pattern = new RegExp(`\\b(window\\.)?${name}(?:\\?\\.)?\\.([A-Za-z_$][\\w$]*)`, 'g'); + const evidence = []; + codeLines.forEach((line, index) => { + for (const match of line.matchAll(pattern)) { + evidence.push({ + file, + line: index + 1, + surface: surfaceFor(file), + access: match[1] ? 'compatibility-global' : 'explicit-provider', + member: match[2], + snippet: rawLines[index].trim().slice(0, 240), + }); + } + }); + return evidence; +}; +const references = new Map(Object.keys(providerFiles).map(name => [name, { production: [], tests: [] }])); +const retiredRendererGlobals = Object.freeze([ + 'globalSettings', + 'applyChatPresentationMode', + 'normalizeChatPresentationMode', + 'checkMessageFilter', + 'applyChatBubbleLayoutSettings', +]); +const publicFacades = Object.freeze({ + VCPMainChatState: Object.freeze({ + owner: 'renderer.js', + dynamicSmoke: 'scripts/test-electron-main-chat-sequences.mjs', + dynamicScript: 'test:electron-main-chat-sequences', + smokeAssertions: Object.freeze([ + 'selectedConfigFrozen: snapshot.selectedItem?.config == null || Object.isFrozen(snapshot.selectedItem.config)', + "hasHistoryRef: 'historyRef' in window.VCPMainChatState", + 'VCPMainChatState must remain a frozen, non-replaceable read-only plugin facade', + ]), + mutability: 'frozen deep read-only snapshot facade; mutation remains in MainChatStateAuthority', + retirement: 'retain while Flowlock and AutoTTS plugin consumers use the main-chat state protocol', + compositionMethodsCheck: false, + }), + MainChatCommands: Object.freeze({ + owner: 'modules/mainChatCommands.js', + dynamicSmoke: 'scripts/test-electron-ui-apps-smoke.mjs', + dynamicScript: 'test:electron-ui-apps', + smokeAssertions: Object.freeze([ + 'const clearResult = window.MainChatCommands.clearNotifications()', + 'assert.deepEqual(parityControls.clearProtection', + ]), + mutability: 'frozen command facade; composition is configured by one-shot event', + retirement: 'retain while Next shell, Classic menus or plugin commands consume it', + }), + VCPAppearanceStudio: Object.freeze({ + owner: 'modules/ui-system/appearance-studio.js', + dynamicSmoke: 'scripts/test-appearance-studio.mjs', + dynamicScript: 'test:appearance-studio', + smokeAssertions: Object.freeze([ + 'const studio = window.VCPAppearanceStudio', + 'assert.equal(studio.open(', + 'assert.equal(studio.isOpen(), true)', + ]), + mutability: 'frozen appearance command facade; composition and disposal use internal events', + retirement: 'retain while Next shell and settings entrypoints consume it', + }), +}); + +for (const file of productionFiles) { + if (!exists(file)) continue; + for (const name of references.keys()) { + references.get(name).production.push(...memberEvidence(file, name)); + } +} + +const facadeLedger = {}; +for (const [name, definition] of Object.entries(publicFacades)) { + const production = []; + const tests = []; + const pattern = new RegExp(`\\bwindow\\.${name}(?:\\?\\.|\\.)([A-Za-z_$][\\w$]*)`, 'g'); + for (const file of productionFiles) { + if (file === definition.owner) continue; + const rawLines = source(file).split(/\r?\n/); + withoutComments(source(file)).forEach((line, index) => { + for (const match of line.matchAll(pattern)) { + production.push({ file, line: index + 1, surface: surfaceFor(file), member: match[1], snippet: rawLines[index].trim().slice(0, 240) }); + } + }); + } + for (const file of testFiles) { + const rawLines = source(file).split(/\r?\n/); + withoutComments(source(file)).forEach((line, index) => { + for (const match of line.matchAll(pattern)) tests.push({ file, line: index + 1, member: match[1], snippet: rawLines[index].trim().slice(0, 240) }); + }); + } + const ownerSource = source(definition.owner); + const smokeSource = source(definition.dynamicSmoke); + assert.ok(production.length > 0, `${name} must have a real production consumer or be retired`); + assert.equal(exists(definition.dynamicSmoke), true, `${name} must name an existing dynamic smoke`); + assert.match(packageScripts[definition.dynamicScript] || '', new RegExp(definition.dynamicSmoke.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), + `${name} dynamic smoke must be wired to npm script ${definition.dynamicScript}`); + for (const assertion of definition.smokeAssertions) { + assert.ok(smokeSource.includes(assertion), `${name} dynamic smoke must retain protocol assertion: ${assertion}`); + } + assert.match(ownerSource, new RegExp(`Object\\.defineProperty\\(window, ['"]${name}['"], \\{\\s*value: Object\\.freeze\\(`), + `${name} must publish a frozen, non-replaceable facade`); + assert.match(ownerSource, /writable:\s*false,\s*configurable:\s*false/, + `${name} window property must be non-writable and non-configurable`); + if (definition.compositionMethodsCheck !== false) { + assert.doesNotMatch(ownerSource, /\b(?:configureCapabilities|setChatManagerProvider)\s*\(/, `${name} must not expose composition methods`); + } + facadeLedger[name] = { ...definition, production, tests }; +} + +const ambientFacadeDefinitions = new Map(); +const ambientFacadeReferences = { production: new Map(), tests: new Map() }; +const recordAmbientDefinition = (name, file, line, syntax, snippet) => { + const definitions = ambientFacadeDefinitions.get(name) || []; + definitions.push({ file, line, syntax, snippet }); + ambientFacadeDefinitions.set(name, definitions); +}; +const recordAmbientReference = (target, name, file, line, snippet) => { + const references = target.get(name) || []; + references.push({ file, line, surface: surfaceFor(file), snippet }); + target.set(name, references); +}; +const indexAmbientFacades = (files, target, includeDefinitions) => { +for (const file of files) { + const rawLines = source(file).split(/\r?\n/); + const codeLines = withoutComments(source(file)); + codeLines.forEach((line, index) => { + const snippet = rawLines[index].trim().slice(0, 240); + for (const match of line.matchAll(/(? left.localeCompare(right))) { + const definitionKeys = new Set(definitions.map(item => `${item.file}:${item.line}`)); + const production = (ambientFacadeReferences.production.get(name) || []) + .filter(item => !definitionKeys.has(`${item.file}:${item.line}`)); + const tests = ambientFacadeReferences.tests.get(name) || []; + ambientFacadeLedger[name] = { + classification: publicFacades[name] ? 'supported-public-facade' : 'legacy-or-feature-local-ambient', + definitions, + production, + tests, + }; +} +for (const name of Object.keys(publicFacades)) { + assert.ok(ambientFacadeLedger[name], `${name} must appear in the complete ambient facade inventory`); +} + +for (const file of productionFiles) { + const code = withoutComments(source(file)).join('\n'); + for (const name of retiredRendererGlobals) { + assert.doesNotMatch(code, new RegExp(`\\bwindow\\.${name}\\b`), `${file} must not consume or publish retired window.${name}`); + } +} +for (const file of testFiles) { + for (const name of references.keys()) { + references.get(name).tests.push(...memberEvidence(file, name)); + } +} +for (const [name, evidence] of references) { + assert.ok(evidence.production.length > 0, `${name} must retain at least one real production member consumer`); + assert.equal(evidence.production.some(item => item.file === providerFiles[name]), false, `${name} provider definition cannot self-certify as a consumer`); + assert.equal( + evidence.production.filter(item => item.access === 'compatibility-global').length, + 0, + `${name} must have zero production compatibility-global consumers after facade retirement` + ); +} + +for (const file of productionFiles) { + const text = source(file); + for (const name of Object.keys(providerFiles)) { + assert.doesNotMatch( + withoutComments(text).join('\n'), + new RegExp(`\\bwindow\\.${name}\\s*(?:\\?\\.)?\\s*[A-Za-z_$][\\w$]*`), + `${file} must not reintroduce window.${name} compatibility access` + ); + } +} + +const kernelFiles = [ + 'modules/chat/chatContext.js', + 'modules/chat/chatRepository.js', + 'modules/chat/contentRuntime.js', + 'modules/chat/contentTransforms.js', + 'modules/chat/chatDomRenderer.js', + 'modules/chat/chatSurface.js', + 'modules/chat/chatOperation.js', + 'modules/chat/chatPresentationState.js', + 'modules/chat/chatPresentationSkin.js', + 'modules/chat/chatSurfaceSlots.js', + 'modules/chat/chatThemePlugin.js', + 'modules/chat/chatPluginManifest.js', + 'modules/chat/streamSession.js', + 'modules/chat/streamCoordinator.js', + 'modules/chat/streamTransientHistory.js', + 'modules/chat/streamConsumerRegistry.js', + 'modules/chat/vcpStreamBridge.js', +]; +// ChatRepository is the intentional transport adapter; the pure-runtime rule +// applies to the domain, content, renderer and plugin contracts around it. +const pureKernelFiles = kernelFiles.filter(file => file !== 'modules/chat/chatRepository.js'); +for (const file of kernelFiles) assert.equal(exists(file), true, `${file} must exist before D1 begins`); + +const forbiddenKernelPatterns = [ + [/\bdocument\b/, 'document'], + [/\bwindow\b/, 'window'], + [/electronAPI/, 'electronAPI'], + [/from ['\"]electron(?:[/'\"])/, 'electron import'], +]; +for (const file of pureKernelFiles) { + const text = source(file); + for (const [pattern, label] of forbiddenKernelPatterns) { + assert.doesNotMatch(text, pattern, `${file} must not depend on ${label}`); + } +} + +const report = { + phase: 'D6', + ignoredProductionDirectoryNames: [...ignoredProductionDirectoryNames], + ignoredProductionDirectoryPaths: [...ignoredProductionDirectoryPaths], + productionFiles, + testFiles, + references: Object.fromEntries(references), + retiredRendererGlobals, + publicFacades: facadeLedger, + ambientFacades: ambientFacadeLedger, + providerFiles, + kernelFiles, + pureKernelFiles, + ownershipSignals: Object.fromEntries(productionFiles.map(file => { + const text = source(file); + return [file, { + windowReferences: count(text, /\bwindow\b/g), + documentReferences: count(text, /\bdocument\b/g), + electronReferences: count(text, /electronAPI/g), + timers: count(text, /\b(?:setTimeout|setInterval|requestAnimationFrame)\b/g), + listeners: count(text, /\.addEventListener\(/g), + mutableMaps: count(text, /new Map\(/g), + }]; + })), + invariants: [ + 'pure kernel files have no document/window/electronAPI dependency', + 'production and test consumers are reported separately', + 'provider definitions, exports and compatibility assignments cannot self-certify as consumers', + 'production consumers and compatibility globals are discovered across all repository production sources', + 'each consumer evidence item records a source line, member access and production surface', + 'legacy facade removal requires a later zero-production-consumer proof', + 'retired renderer globals have zero production definitions and consumers', + 'each retained public facade is frozen, has production consumers, a dynamic smoke and a retirement decision', + 'all direct production window facade publications are inventoried with definitions and consumers', + 'main-window start/data/end/error events have one coordinator authority', + ], +}; +const rendererSource = source('renderer.js'); +const settingsPresentationOwnerSource = source('modules/renderer/mainChatSettingsPresentationOwner.js'); +const messageRendererSource = source('modules/messageRenderer.js'); +const mainChatEventBridgeSource = source('modules/renderer/mainChatEventBridge.js'); +const nonStreamingEventConsumerSource = source('modules/renderer/nonStreamingEventConsumer.js'); +const contentProcessorSource = source('modules/renderer/contentProcessor.js'); +const chatManagerSource = source('modules/chatManager.js'); +const settingsOwnerSource = source('modules/renderer/mainChatSettingsOwner.js'); +const ownedPreloadSource = source('modules/renderer/ownedPreloadSubscription.js'); +const uiManagerSource = source('modules/uiManager.js'); +const eventListenersSource = source('modules/event-listeners.js'); +const appearanceStudioSource = source('modules/ui-system/appearance-studio.js'); +assert.match(messageRendererSource, /export function createMessageRenderer\(options = \{\}\) \{[\s\S]*const surfaceId = String\(/, + 'MessageRenderer must create a stable per-instance Surface namespace'); +assert.match(messageRendererSource, /const ownedStyleElements = new Set\(\)/, + 'MessageRenderer must own injected style nodes per renderer instance'); +assert.match(messageRendererSource, /for \(const styleElement of ownedStyleElements\)/, + 'MessageRenderer style cleanup must use instance-owned nodes'); +const streamHandlerStart = rendererSource.indexOf('chatAPI.onVCPStreamEvent'); +const streamHandlerEnd = rendererSource.indexOf('chatAPI.onVCPGroupTopicUpdated', streamHandlerStart); +const streamHandlerSource = rendererSource.slice(streamHandlerStart, streamHandlerEnd < 0 ? undefined : streamHandlerEnd); +assert.doesNotMatch(streamHandlerSource, /messageRenderer\.appendStreamChunk\(messageId/, 'renderer must not retain direct stream chunk dispatch'); +assert.doesNotMatch(streamHandlerSource, /messageRenderer\.finalizeStreamedMessage\(\s*messageId/, 'renderer must not retain direct stream terminal dispatch'); +assert.match(rendererSource, /mainChatAdapter\?\.acceptStreamEvent\(eventData\)/, 'main window must route VCP events through MainChatSurfaceAdapter'); +assert.match(rendererSource, /createMainChatSettingsPresentationOwner/, 'renderer must construct the settings presentation owner'); +assert.match(rendererSource, /createMainChatAttachmentOwner/, 'renderer must construct the attachment state owner'); +assert.match(rendererSource, /createMainChatSendOwner/, 'renderer must construct the send and interrupt owner'); +assert.match(rendererSource, /attachedFilesRef: mainChatAttachmentOwner\.ref/, 'ChatManager must receive the owned attachment ref'); +assert.match(rendererSource, /sendButtonAction: mainChatSendOwner\.handleAction/, 'event listeners must receive the owned send action'); +assert.match(rendererSource, /notifySendStateChanged: mainChatSendOwner\.update/, 'stream composition must receive the owned send projection'); +assert.match(rendererSource, /currentSelectedItemRef = mainChatStateAuthority\.selectedItemRef/, 'renderer must consume the selection authority ref'); +assert.match(rendererSource, /currentTopicIdRef = mainChatStateAuthority\.topicIdRef/, 'renderer must consume the topic authority ref'); +assert.doesNotMatch(rendererSource, /\blet\s+(?:attachedFiles|currentSelectedItem|currentTopicId)\b/, + 'renderer must not regain mutable attachment or conversation mirrors'); +assert.doesNotMatch(rendererSource, /function\s+(?:getInterruptibleMessageForCurrentChat|updateSendButtonState|interruptActiveResponseFromSendButton|handleSendButtonAction)\b/, + 'renderer must not regain send or interrupt business policy'); +for (const [file, text] of [ + ['modules/chatManager.js', chatManagerSource], + ['modules/settingsManager.js', source('modules/settingsManager.js')], + ['modules/messageRenderer.js', messageRendererSource], +]) { + assert.doesNotMatch( + withoutComments(text).join('\n'), + /\b(?:currentSelectedItem|selectedItem|currentSelectedItemVal)\.(?:name|config|topics|uiCollapseStates|avatarUrl|avatarCalculatedColor)\s*=|Object\.assign\((?:currentSelectedItem|selectedItem)\b/, + `${file} must replace selection state through its authority instead of mutating borrowed values` + ); +} +assert.doesNotMatch( + withoutComments(messageRendererSource).join('\n'), + /const\s+currentChatHistoryArray\s*=\s*mainRendererReferences\.currentChatHistoryRef\.get\(\)[\s\S]{0,400}?currentChatHistoryArray\.splice\(/, + 'MessageRenderer must copy history snapshots before mutation' +); +assert.match(rendererSource, /\[\.\.\.ownedRendererSubscriptions\]\.reverse\(\)[\s\S]*for \(const subscription of subscriptions\)[\s\S]*await subscription\.dispose\(\)/, + 'renderer composition must await owned capability disposal in reverse registration order'); +assert.match(ownedPreloadSource, /const tasks = new Set\(\)[\s\S]*await Promise\.allSettled\(\[\.\.\.tasks\]\)/, + 'owned preload subscriptions must drain in-flight async consumers during disposal'); +assert.match(ownedPreloadSource, /abortController\.abort\(\)/, + 'owned preload subscriptions must signal lifecycle cancellation before draining consumers'); +assert.match(chatManagerSource, /async function dispose\(\)[\s\S]*emptyStateObserver\?\.disconnect\(\)[\s\S]*canvasContentDisposer\?\.\(\)[\s\S]*await Promise\.allSettled/, + 'ChatManager must retract DOM/preload resources and drain owned persistence work'); +assert.match(rendererSource, /ownedRendererSubscriptions\.add\(chatManager\)/, + 'renderer composition must register ChatManager teardown'); +assert.match(uiManagerSource, /themeDisposer = electronAPI\.onThemeUpdated[\s\S]*async \(\) => \{[\s\S]*themeDisposer\?\.\(\)[\s\S]*await Promise\.allSettled/, + 'UIManager must own its theme subscription and drain async projection work'); +assert.match(rendererSource, /ownedRendererSubscriptions\.add\(\{ dispose: \(\) => window\.uiManager\.dispose\?\.\(\) \}\)[\s\S]*await window\.uiManager\.init/, + 'renderer must register UIManager disposal before awaiting initialization'); +assert.match(settingsOwnerSource, /const get = \(\) => clone\(settings\)/, + 'settings authority reads must return detached values'); +assert.match(settingsOwnerSource, /const snapshot = \(\) => freeze\(clone\(settings\)\)/, + 'settings authority snapshots must be recursively frozen'); +for (const [file, text] of [ + ['modules/event-listeners.js', eventListenersSource], + ['modules/uiManager.js', uiManagerSource], + ['modules/ui-system/appearance-studio.js', appearanceStudioSource], +]) { + assert.doesNotMatch( + withoutComments(text).join('\n'), + /(?:globalSettings|currentSettings|settings)\.(?:sidebarAvatarOnly|sidebarActive|assistantEnabled|currentThemeMode|appearanceProfile|filterEnabled)\s*=|Object\.assign\(getSettings\(\)/, + `${file} must commit settings changes through the settings authority` + ); +} +assert.match(rendererSource, /mainChatSettingsPresentationOwner\.loadAndApply\(\)/, + 'renderer must delegate settings loading and projection to its owner'); +assert.doesNotMatch(rendererSource, /CHAT_FONT_PRESETS|function captureChatPresentationScrollAnchor|function syncChatPresentationModeControls/, + 'renderer must not regain settings or presentation DOM policy'); +assert.match(settingsPresentationOwnerSource, /async function loadAndApply\(\)/, + 'settings presentation owner must own startup settings projection'); +assert.doesNotMatch(rendererSource, /window\.(?:globalSettings|applyChatPresentationMode|normalizeChatPresentationMode|checkMessageFilter|applyChatBubbleLayoutSettings)/, + 'renderer must not republish retired ambient settings and presentation globals'); +// These assertions require production composition evidence; test-only imports cannot satisfy them. +assert.match(rendererSource, /createMainChatEventBridge/, 'renderer must construct the main chat event bridge'); +assert.match(mainChatEventBridgeSource, /chatAPI\.onVCPStreamEvent/, 'event bridge must consume the preload producer'); +assert.match(mainChatEventBridgeSource, /subscription\?\.dispose|subscription\(\)/, 'event bridge must retain a producer disposer'); +assert.match(rendererSource, /createNonStreamingEventConsumer/, 'renderer must construct the non-streaming event consumer'); +assert.match(nonStreamingEventConsumerSource, /renderFullMessageProjection/, 'non-streaming consumer must project durable full responses'); +assert.match(nonStreamingEventConsumerSource, /renderTarget\.removeMessage/, 'non-streaming consumer must remove through its owning Surface'); +assert.match(contentProcessorSource, /mainRefs\.messageCommands\?\.handleSendMessage/, 'content processor must use an injected Surface send capability'); +assert.doesNotMatch(contentProcessorSource, /getElementById\(['"]messageInput['"]\)/, 'content processor must not discover the main input by global DOM id'); +assert.match(chatManagerSource, /renderTarget\?\.removeMessage(?:ById)?/, 'ChatManager must remove placeholders through the initiating render target'); +assert.doesNotMatch(chatManagerSource, /window\.updateSendButtonState/, 'ChatManager must use the injected send-state capability'); +assert.doesNotMatch(source('modules/event-listeners.js'), /window\.handleSendButtonAction|window\.__vcpCancelActiveResponse/, 'event listeners must use the injected send-button capability'); +assert.match(messageRendererSource, /vcp-\$\{surfaceId\}-chat-\$\{message\.id\}/, 'message styles must be namespaced by Surface and message'); +assert.match(messageRendererSource, /audioRoot\?\.querySelectorAll\?\.\(['"]audio\.vcp-audio-native['"]\)/, 'audio playback isolation must be scoped to the owning Surface root'); +assert.doesNotMatch(source('modules/renderer/streamManager.js'), /onStreamStateChanged/, 'stream projection must not retain the legacy generic state callback'); +assert.doesNotMatch(source('modules/renderer/streamManager.js'), /notifySurfaceOperationStateChanged|updateSendButtonState/, + 'stream projection must not reverse-control main-window send state'); +assert.match(chatManagerSource, /notifySendStateChanged/, 'ChatManager operation owner must publish main send-state changes'); +assert.match(source('modules/renderer/streamManager.js'), /transientStreamHistory/, 'stream projection history must be delegated to the transient history provider'); +assert.match(source('modules/renderer/streamManager.js'), /streamOperationId/, 'stream projection must carry producer operation identity into its Surface runtime'); +assert.match(source('modules/renderer/streamManager.js'), /createStreamProjectionRuntime/, 'stream projection runtime state must use the Surface-owned operation-scoped runtime provider'); +assert.doesNotMatch(source('modules/renderer/streamManager.js'), /refs\.historyAuthority/, 'stream projection must not use an ambiguous durable-history authority name'); +assert.match(rendererSource, /createStreamTransientHistory[\s\S]*transientStreamHistory,/, 'main and internal composition must construct and inject transient history providers'); +assert.match(source('Voicechatmodules/voicechat.js'), /createStreamTransientHistory[\s\S]*transientStreamHistory,[\s\S]*viewAuthority/, 'Voice Surface must own its transient history and view authority'); +assert.match(source('rust_assistant_engine/ui/assistant.js'), /createStreamTransientHistory[\s\S]*transientStreamHistory,[\s\S]*viewAuthority/, 'Rust assistant Surface must own its transient history and view authority'); +assert.doesNotMatch(messageRendererSource, /createStreamTransientHistory/, 'MessageRenderer must consume rather than secretly construct the transient history provider'); +assert.match(source('modules/renderer/streamManager.js'), /requires an owning Surface root/, 'stream projection must fail fast without an owning root'); +assert.match(source('modules/renderer/streamManager.js'), /requires an explicit view authority/, 'stream projection must fail fast without a view authority'); +assert.doesNotMatch(source('modules/renderer/streamManager.js'), /if \(refs\.viewAuthority &&/, 'stream projection must not silently fall back to global selection state'); +const reportPath = path.join(root, 'docs/chat-kernel-consumer-report.json'); +fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); +console.log(`Chat Kernel consumer baseline passed (${productionFiles.length} production files, ${testFiles.length} test files, ${kernelFiles.length} kernel files).`); +for (const [name, value] of references) { + const files = new Set(value.production.map(item => item.file)); + const compatibility = value.production.filter(item => item.access === 'compatibility-global').length; + console.log(` ${name}: productionEvidence=${value.production.length}, productionFiles=${files.size}, compatibility=${compatibility}, tests=${value.tests.length}`); +} diff --git a/scripts/check-chat-release-evidence.mjs b/scripts/check-chat-release-evidence.mjs new file mode 100644 index 000000000..da69cbd0e --- /dev/null +++ b/scripts/check-chat-release-evidence.mjs @@ -0,0 +1,26 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const required = [ + 'VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT', + 'VCPCHAT_UI_APPS_EVIDENCE_OUTPUT', + 'VCPCHAT_WINDOWS_MATRIX_EVIDENCE_INPUT', + 'VCPCHAT_MANUAL_SOAK_EVIDENCE_INPUT', + 'VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT', +]; +const missing = required.filter(name => !process.env[name]); +if (missing.length) { + console.error(`Chat release evidence gate failed: missing ${missing.join(', ')}`); + process.exit(1); +} +const output = process.argv[2] || path.join('artifacts', 'contracts', 'chat-release-evidence-manifest.json'); +const result = spawnSync(process.execPath, ['scripts/write-chat-evidence-manifest.mjs', output], { + cwd: root, + env: { ...process.env, VCPCHAT_REAL_ELECTRON_STATUS: 'pass' }, + encoding: 'utf8', + stdio: 'inherit', +}); +if (result.status !== 0) process.exit(result.status || 1); +console.log(`Chat release evidence gate passed (${output}).`); diff --git a/scripts/check-design-system-boundary.mjs b/scripts/check-design-system-boundary.mjs index 31bdf4267..ba8349ac5 100644 --- a/scripts/check-design-system-boundary.mjs +++ b/scripts/check-design-system-boundary.mjs @@ -9,8 +9,12 @@ const root = process.cwd(); // local `upstream/main` ref may be older still. Both make accepted product // changes look like design-system violations. Environment overrides remain // available when a PR intentionally audits against a newly reviewed snapshot. -const sourceRef = process.env.VCP_DESIGN_SOURCE_REF || '3da77f004936808483dbf773e0a2e7b3a5d1eadc'; -const upstreamRef = process.env.VCP_UPSTREAM_REF || sourceRef; +const sourceRef = process.env.VCP_DESIGN_SOURCE_REF || 'b5931a69d0815a1dfd60c079093ed5518a73dc77'; +// Compare the reviewed subtraction snapshot against the current product main +// as the second ancestry boundary. The snapshot is intentionally not itself +// the upstream ref: this branch may contain unrelated upstream product work +// that is already present on main and must not be reported as a design delta. +const upstreamRef = process.env.VCP_UPSTREAM_REF || 'origin/main'; const failures = []; const forbiddenPaths = [ @@ -34,13 +38,18 @@ const forbiddenPaths = [ const allowedSourceDifferences = new Set([ '.github/workflows/canonical_ui.yml', + '.github/workflows/chat_kernel_ui.yml', '.gitattributes', '.gitignore', 'README.md', 'RAGmodules/RAG_Observer.html', + // Deletion side of the 2026-08 documentation archive moves. Current + // content is allowed only through the archive path pattern below. 'docs/next-ui-webawesome-roadmap.md', 'docs/next-ui-lifecycle-architecture.md', 'docs/next-ui-development-roadmap.md', + 'docs/deepseek-harness-ui-ux-research.md', + 'docs/ui-interaction-accessibility-roadmap.md', 'docs/next-ui-current-state.md', 'docs/classic-retirement-architecture.md', 'docs/classic-retirement-inventory.md', @@ -100,17 +109,33 @@ const allowedSourceDifferences = new Set([ 'modules/event-listeners.js', 'modules/filterManager.js', 'modules/global-settings-manager.js', + 'modules/inputEnhancer.js', + 'modules/notificationRenderer.js', + 'modules/settingsManager.js', + 'modules/weatherService.js', 'modules/itemListManager.js', 'modules/ipc/deepWikiHandlers.js', 'modules/ipc/chatHandlers.js', 'modules/ipc/canvasHandlers.js', 'modules/ipc/desktopHandlers.js', 'modules/ipc/agentHandlers.js', + 'modules/ipc/assistantHandlers.js', + 'modules/ipc/voiceHandlers.js', 'modules/ipc/settingsHandlers.js', 'modules/ipc/themeHandlers.js', 'modules/ipc/windowHandlers.js', 'modules/mainChatCommands.js', 'modules/messageRenderer.js', + 'modules/renderer/mainChatComposition.js', + 'modules/renderer/mainChatDomBindings.js', + 'modules/renderer/streamProjectionRuntime.js', + 'modules/renderer/topicSelectionReadiness.js', + 'modules/renderer/ownedPreloadSubscription.js', + 'modules/renderer/mainChatEventBridge.js', + 'modules/renderer/nonStreamingEventConsumer.js', + 'modules/renderer/animation.js', + 'modules/renderer/contentPipeline.js', + 'modules/renderer/contentProcessor.js', 'modules/notificationRenderer.js', 'modules/renderer/messageContextMenu.js', 'modules/renderer/streamManager.js', @@ -147,7 +172,6 @@ const allowedSourceDifferences = new Set([ 'modules/ui-system/next-shell/app-tab-host.js', 'modules/ui-system/next-shell/assistant-search-controller.js', 'modules/ui-system/next-shell/account-menu-controller.js', - 'modules/ui-system/next-shell/escape-dispatcher.js', 'modules/ui-system/next-shell/launchpad-controller.js', 'modules/ui-system/next-shell/creation-controller.js', 'modules/ui-system/next-shell/next-shell-controller.js', @@ -231,6 +255,7 @@ const allowedSourceDifferences = new Set([ 'scripts/test-electron-ui-apps-smoke.mjs', 'scripts/test-electron-lifecycle-stress.mjs', 'scripts/test-electron-main-chat-sequences.mjs', + 'scripts/test-electron-windows-matrix.mjs', 'scripts/test-settings-wa-electron.mjs', 'tests/frontend-plugins.test.js', 'tests/lifecycle-scope.test.js', @@ -247,8 +272,6 @@ const allowedSourceDifferences = new Set([ 'tests/app-tab-host.test.js', 'tests/assistant-search-controller.test.js', 'tests/account-menu-controller.test.js', - 'tests/escape-dispatcher.test.js', - 'tests/notification-menu-controller.test.js', 'tests/launchpad-controller.test.js', 'tests/creation-controller.test.js', 'tests/global-settings-save.test.mjs', @@ -262,7 +285,22 @@ const allowedSourceDifferences = new Set([ 'tests/history-watcher-lease-manager.test.js', 'tests/message-edit-watcher-failure.test.js', 'tests/stream-manager-terminal-cleanup.test.js', + 'tests/desktop-push-consumer.test.mjs', + 'tests/emoticon-fixer-owner.test.mjs', + 'tests/image-handler-owner.test.mjs', + 'tests/middle-click-owner.test.mjs', + 'tests/render-session-authority.test.mjs', + 'tests/surface-conversation.test.mjs', + 'tests/surface-task-owner.test.mjs', + 'tests/visibility-optimizer-owner.test.mjs', 'tests/main-chat-sequence-model.test.js', + 'tests/main-chat-event-bridge.test.mjs', + 'tests/main-chat-dom-bindings.test.mjs', + 'tests/main-chat-state-authority.test.mjs', + 'tests/topic-selection-readiness.test.mjs', + 'tests/owned-preload-subscription.test.mjs', + 'tests/non-streaming-event-consumer.test.mjs', + 'tests/stream-transient-history.test.mjs', 'tests/settlement.test.js', 'tests/settings-settlement.test.js', 'tests/vcpchat-bootstrap.test.mjs', @@ -272,11 +310,11 @@ const allowedSourceDifferences = new Set([ 'styles/notifications.css', 'styles/animations.css', 'styles/layout.css', - 'styles/chat.css', 'styles/compact-sidebar.css', 'styles/components.css', 'styles/settings.css', 'styles/themes.css', + 'styles/chat.css', 'styles/appearance.css', 'styles/setting/settings-global-modal.css', 'styles/themes/themes纸墨与机芯.css', @@ -302,9 +340,65 @@ const allowedSourceDifferences = new Set([ 'assets/nova_button_light.png', 'assets/svg/acrylic-noise.svg', 'Notemodules/notes.js', + 'renderer.js', + 'modules/messageRenderer.js', + 'modules/renderer/streamManager.js', + 'modules/chat/streamTransientHistory.js', + 'modules/renderer/desktopPushConsumer.js', + 'modules/renderer/domBuilder.js', + 'modules/renderer/emoticonUrlFixer.js', + 'modules/renderer/enhancedColorUtils.js', + 'modules/renderer/imageHandler.js', + 'modules/renderer/renderSessionAuthority.js', + 'modules/renderer/surfaceTaskOwner.js', + 'modules/renderer/visibilityOptimizer.js', + 'modules/text-viewer.js', + 'scripts/check-chat-kernel-consumers.mjs', + 'docs/chat-kernel-consumer-report.json', + 'docs/chat-kernel-deep-decoupling-roadmap.md', + 'docs/chat-kernel-vd7-final-audit.md', + 'docs/deepseek-harness-plugin-ui-architecture-research.md', + 'docs/chat-kernel-rendering-roadmap.md', + 'Voicechatmodules/voicechat.js', + 'Voicechatmodules/voicechat.html', + 'rust_assistant_engine/ui/assistant.js', + 'rust_assistant_engine/ui/assistant.html', + 'Flowlockmodules/flowlock-integration.js', + 'Flowlockmodules/flowlock.js', + 'VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js', + 'modules/event-listeners.js', + 'modules/renderer/middleClickHandler.js', ]); const allowedSourceDifferencePatterns = [ + /^docs\/archive\/2026-08-chat-kernel-and-ui-roadmaps\//, + // Chat Kernel D5/D6 owner modules and focused lifecycle tests belong to + // this review slice, not to the design-system subtraction leak set. + /^modules\/renderer\/(?:domListenerOwner|forwardMessageOwner|mainChatAttachmentOwner|mainChatAuxiliaryEventOwner|mainChatFlowlockOwner|mainChatSendOwner|mainChatSettingsOwner|mainChatThemeOwner|ttsSurfaceOwner)\.js$/, + /^tests\/(?:dom-listener-owner|enhanced-color-utils-lifecycle|forward-message-owner|input-enhancer-owner|main-chat-attachment-owner|main-chat-auxiliary-event-owner|main-chat-flowlock-owner|main-chat-send-owner|main-chat-theme-owner|notification-renderer-lifecycle|tts-surface-owner|ui-manager-lifecycle)\.test\.(?:js|mjs)$/, + /^scripts\/test-electron-manual-soak\.mjs$/, + // Generated evidence is audited output, not product UI source. + /^artifacts\/(?:manual-soak|windows-matrix)\/[^/]+\.json$/, + // Contract/evidence automation is a gate layer and must not be treated as + // a design-system subtraction delta. + /^docs\/contracts\//, + /^docs\/chat-kernel-evidence-and-contracts-roadmap\.md$/, + /^docs\/chat-event-producer-consumer-roadmap\.md$/, + /^docs\/stream-switch-tool-wait-recovery\.md$/, + /^scripts\/(?:build-chat-event-graph|check-chat-contracts|check-chat-evidence|check-artifact-plane|run-chat-contract-invariants|test-built-artifact-smoke|test-chat-contract-invalid|test-chat-transcript-snapshot|write-chat-evidence-manifest)\.mjs$/, + // Evidence-plane runners are release contracts, not UI subtraction code. + /^scripts\/(?:check-chat-release-evidence|test-artifact-plane-invalid|test-chat-evidence-manifest|test-chat-release-evidence-invalid|test-facade-registry-invalid|test-packaged-artifact-invalid|test-packaged-artifact-smoke)\.mjs$/, + /^scripts\/fixtures\/chat-contract-invalid\.mjs$/, + /^tests\/chat-event-contract\.test\.mjs$/, /^vendor\/webawesome(?:-runtime)?\//, + /^modules\/chat\//, + /^modules\/renderer\/(?:mainChatComposition|mainChatSettingsPresentationOwner|mainChatStreamConsumer|mainChatSurfaceAdapter|renderDependencies|windowStreamRuntime)\.js$/, + /^tests\/(?:notification-renderer-lifecycle|main-chat-settings-owner)\.test\.mjs$/, + /^modules\/ui-system\/(?:standalone-chat-app|interactive-chat-app)\.js$/, + /^Voicechatmodules\/voicechat\.js$/, + /^rust_assistant_engine\/ui\/assistant\.(?:js|html)$/, + /^Flowlockmodules\/flowlock-integration\.js$/, + /^modules\/event-listeners\.js$/, + /^tests\/(?:chat-|content-|stream-|memory-chat-repository|window-stream-runtime|render-dependencies|main-chat-surface-adapter|vcp-stream-bridge)/, /^(?:Agenttaskmodules|Forummodules|Logmodules|Memomodules|PluginManagerModules|VCPHumanToolBox|VchatManager)\//, /^Notemodules\/notemini\.(?:html|js|css)$/, ]; diff --git a/scripts/check-next-delta-contract.mjs b/scripts/check-next-delta-contract.mjs index f169cee92..41b881307 100644 --- a/scripts/check-next-delta-contract.mjs +++ b/scripts/check-next-delta-contract.mjs @@ -26,6 +26,8 @@ for (const retiredFile of [ assert.equal(document.documentElement.dataset.uiMode, 'next', 'main.html must declare the canonical presentation'); assert.doesNotMatch(read('preloads/shared/catalog.js'), /onUiModeUpdated|ui-mode-updated/, 'preload must not expose a presentation subscription without a sender and consumer'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /\binitStreamManager\b/, + 'StreamProjection must not retain the retired initStreamManager compatibility facade'); const canonicalIds = [ 'nextUiTopbar', 'nextUiAddTabBtn', 'nextUiCreateItemBtn', @@ -65,6 +67,37 @@ for (const file of mainRuntimeFiles) { } const rendererSource = read('renderer.js'); +assert.match(rendererSource, /createMainChatDomBindings\(document\)/, + 'renderer composition must resolve its fixed main-window DOM through one explicit adapter'); +assert.match(rendererSource, /createTopicSelectionReadiness\(\)/, + 'renderer composition must own the topic-selection readiness capability'); +assert.doesNotMatch(rendererSource, /__vcpRendererReady|__vcpPendingTopicSelection/, + 'renderer must not publish topic readiness through ambient window fields'); +assert.doesNotMatch(rendererSource, /const chatMessagesDiv = document\.getElementById|const messageInput = document\.getElementById|const sendMessageBtn = document\.getElementById/, + 'renderer must not recreate canonical chat DOM bindings outside the composition adapter'); +assert.match(read('modules/renderer/mainChatDomBindings.js'), /export function createMainChatDomBindings\(document\)[\s\S]*Object\.freeze\(bindings\)/, + 'MainChatDomBindings must expose an immutable, document-owned capability closure'); +const middleClickSource = read('modules/renderer/middleClickHandler.js'); +assert.doesNotMatch(middleClickSource, /window\.(?:showForwardModal|ensureAudioContext)/, + 'middle-click actions must use Surface-injected forward/audio capabilities'); +assert.doesNotMatch(read('modules/renderer/messageContextMenu.js'), /ownerWindow\.ensureAudioContext/, + 'context-menu audio activation must use its Surface capability'); +assert.match(read('modules/renderer/streamProjectionRuntime.js'), /export function createStreamProjectionRuntime\(\)[\s\S]*return \{[\s\S]*messageRuntimeKeys/, + 'StreamProjection mutable maps must be created by an explicit Surface-owned runtime provider'); +assert.doesNotMatch(read('modules/renderer/streamProjectionRuntime.js'), /\b(?:window|document|electronAPI)\b/, + 'StreamProjectionRuntime must remain independent from DOM and Electron'); +for (const file of ['renderer.js', 'Flowlockmodules/flowlock-integration.js', 'Flowlockmodules/flowlock.js', 'VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js']) { + assert.doesNotMatch(read(file), /window\.(?:currentSelectedItem|currentTopicId)\b/, + `${file} must consume the read-only VCPMainChatState capability instead of mutable selection globals`); +} +assert.match(rendererSource, /Object\.defineProperty\(window, ['"]VCPMainChatState['"], \{[\s\S]*value: Object\.freeze\(mainChatStateAuthority\.consumer\)[\s\S]*writable: false,[\s\S]*configurable: false/, + 'the composition root must expose a frozen, non-replaceable read-only main-chat state consumer'); +assert.doesNotMatch(read('modules/chat/mainChatStateAuthority.js'), /\b(?:window|document|electronAPI)\b/, + 'main chat state authority must remain independent from DOM and Electron'); +assert.match(read('modules/topicListManager.js'), /topicSelectionReadiness\.isReady\(\)[\s\S]*topicSelectionReadiness\.defer/, + 'TopicListManager must consume the injected readiness capability'); +assert.doesNotMatch(read('modules/topicListManager.js'), /__vcpRendererReady|__vcpPendingTopicSelection/, + 'TopicListManager must not read or write ambient renderer readiness fields'); const globalSettingsSource = read('modules/global-settings-manager.js'); const appearanceStudioSource = read('modules/ui-system/appearance-studio.js'); for (const [file, source] of [ @@ -122,6 +155,232 @@ const itemListSource = read('modules/itemListManager.js'); assert.doesNotMatch(itemListSource, /assistant-catalog|getCatalogState|getCatalogSnapshot|whenSettled|catalogChannel/, 'item list must not publish a test-only catalog store'); +const chatRepositorySource = read('modules/chat/chatRepository.js'); +assert.match(chatRepositorySource, /createChatRepository/, + 'ChatRepository must define the explicit history capability boundary'); +assert.match(read('renderer.js'), /createChatRepository\(chatAPI\)[\s\S]*?chatRepository/, + 'ChatRepository must have a real main renderer consumer'); +assert.match(read('renderer.js'), /topicListManager\.init\([\s\S]*?chatRepository,/, + 'TopicListManager must receive the shared ChatRepository at its production entry'); +assert.match(read('renderer.js'), /searchManager\.init\([\s\S]*?chatRepository,/, + 'SearchManager must receive the shared ChatRepository at its production entry'); +assert.match(read('renderer.js'), /createStreamTransientHistory\([\s\S]*?repository: chatRepository/, + 'the composition root must provide transient stream history from the shared ChatRepository'); +assert.match(read('modules/messageRenderer.js'), /transientStreamHistory: mainRendererReferences\.transientStreamHistory/, + 'MessageRenderer must pass through the injected transient-history provider'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /chatRepository/, + 'StreamProjection must not retain direct repository authority'); +const historyPersistenceSource = read('modules/chat/chatHistoryPersistence.js'); +assert.match(historyPersistenceSource, /createHistoryPersistence[\s\S]*repository\.getHistory[\s\S]*repository\.saveHistory/, + 'ChatHistoryPersistence must own the durable stream read/merge/write policy'); +assert.match(historyPersistenceSource, /createTransientChatHistoryPersistence[\s\S]*durable: false/, + 'auxiliary terminal persistence must explicitly identify its transient session contract'); +for (const auxiliaryWindow of ['Voicechatmodules/voicechat.js', 'rust_assistant_engine/ui/assistant.js']) { + const auxiliarySource = read(auxiliaryWindow); + assert.match(auxiliarySource, /createTransientChatHistoryPersistence/, + `${auxiliaryWindow} must not describe its short-lived session repository as durable`); + assert.match(auxiliarySource, /await streamRuntime\?\.cancel\(activeStreamingMessageId,/, + `${auxiliaryWindow} must await its real stream operation before durable close save`); + assert.doesNotMatch(auxiliarySource, /waitForActiveStreamToSettle|Timed out while waiting stream to settle/, + `${auxiliaryWindow} must not reintroduce polling settlement`); +} +const assistantHandlersSource = read('modules/ipc/assistantHandlers.js'); +assert.match(assistantHandlersSource, /did-finish-load/, 'assistant context must wait for renderer load'); +assert.match(assistantHandlersSource, /send\(['"]assistant-data['"]/, 'assistant context must be sent to the loaded renderer'); +assert.doesNotMatch(historyPersistenceSource, /\b(?:window|document|electronAPI)\b/, + 'ChatHistoryPersistence must remain independent from DOM and Electron'); +assert.match(read('modules/chat/chatDomRenderer.js'), /createChatDomRenderer/, + 'ChatDomRenderer must define the explicit root/teardown adapter'); +assert.match(read('modules/chat/contentRuntime.js'), /createRenderModel/, + 'ContentRuntime must expose a DOM-free render-model operation'); +assert.doesNotMatch(read('modules/chat/contentRuntime.js'), /from ['"]\.\.\/renderer\//, + 'ContentRuntime must not depend on renderer implementation modules'); +assert.doesNotMatch(read('modules/chat/chatSurface.js'), /presentationState\?\.subscribe\?\.\(\(\) => \{\}\)/, + 'ChatSurface must not create a test-only no-op state subscription'); +assert.match(read('modules/renderer/streamManager.js'), /createRenderModel\([\s\S]*PIPELINE_MODES\.STREAM_FAST/, + 'StreamManager must consume the DOM-free ContentRuntime render model'); +assert.match(read('modules/messageRenderer.js'), /cleanupAnimationsInContent\(contentDiv\)[\s\S]*unobserveMessage\(messageItem\)/, + 'message teardown must release animation and visibility resources per root'); +assert.match(read('modules/messageRenderer.js'), /vcpAudioCleanup[\s\S]*listenerDisposers[\s\S]*audio\.removeAttribute\('src'\)/, + 'audio controls must remove listeners and release media sources on root teardown'); +assert.match(read('modules/messageRenderer.js'), /disposeRootResources[\s\S]*cleanupMessageDomResources/, + 'independent ChatSurface roots must have an explicit resource disposer'); +assert.match(read('modules/messageRenderer.js'), /createRenderSessionAuthority[\s\S]*invalidateRenderSession\(root\)/, + 'message rendering must revoke progressive work per owning Surface root'); +assert.doesNotMatch(read('modules/messageRenderer.js'), /let activeRenderSessionId\s*=/, + 'message rendering must not retain a module-wide scalar render generation'); +assert.match(read('modules/messageRenderer.js'), /export function createMessageRenderer\(options = \{\}\)/, + 'message rendering must be created by an explicit Surface-owned factory'); +assert.doesNotMatch(read('modules/messageRenderer.js'), /export const messageRenderer\s*=/, + 'message rendering must not expose a shared mutable singleton instance'); +for (const [file, factory] of [ + ['modules/renderer/imageHandler.js', 'createImageHandler'], + ['modules/renderer/emoticonUrlFixer.js', 'createEmoticonUrlFixer'], + ['modules/renderer/contentProcessor.js', 'createContentProcessor'], + ['modules/renderer/messageContextMenu.js', 'createMessageContextMenu'], + ['modules/renderer/middleClickHandler.js', 'createMiddleClickHandler'], + ['modules/renderer/visibilityOptimizer.js', 'createVisibilityOptimizer'], +]) { + assert.match(read(file), new RegExp(`export function ${factory}\\(`), + `${file} must expose a renderer-owned provider factory`); +} +for (const factory of ['createImageHandler', 'createVisibilityOptimizer', 'createEmoticonUrlFixer', 'createContentProcessor', 'createMessageContextMenu', 'createMiddleClickHandler']) { + assert.match(read('modules/messageRenderer.js'), new RegExp(`${factory}\\(`), + `each MessageRenderer must construct its own ${factory} provider`); +} +assert.doesNotMatch(read('modules/renderer/imageHandler.js'), /export (?:const|let|var) (?:imageHandlerRefs|ownedContentListeners)/, + 'image interaction ownership must not be exported as mutable module state'); +assert.doesNotMatch(read('modules/renderer/emoticonUrlFixer.js'), /export (?:const|let|var) (?:emoticonLibrary|initializationPromise)/, + 'emoticon catalog ownership must not be exported as mutable module state'); +assert.match(read('modules/renderer/visibilityOptimizer.js'), /visibilityOwnerByMessage\.get\(messageItem\)\?\.captureWebAnimation/, + 'the realm animation interceptor may route only through the exact message owner'); +assert.match(read('modules/renderer/visibilityOptimizer.js'), /scanTimers[\s\S]*unobserveMessage[\s\S]*scanTimer\.window\.clearTimeout\(scanTimer\.id\)/, + 'visibility teardown must cancel delayed per-message scans'); +assert.match(read('modules/renderer/animation.js'), /cleanupAnimationsInContent[\s\S]*renderer\.dispose\(\)/, + 'animation cleanup must dispose Three.js renderers'); +assert.match(read('modules/renderer/animation.js'), /_vcpMutationObserver[\s\S]*disconnect/, + 'Three.js DOM observers must disconnect with their owning renderer'); +assert.match(read('modules/renderer/contentProcessor.js'), /scheduleOwnedTimeout[\s\S]*clearOwnedTimeouts[\s\S]*cleanupPreviewsInContent/, + 'content processor delayed work must be owned by the message root and cleared on teardown'); +assert.match(read('modules/chat/chatOperation.js'), /cancelRequested[\s\S]*if \(cancelRequested\) return true/, + 'interactive surface cancellation must be idempotent for repeated user actions'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /historySaveQueue|historySaveChains|saveHistoryForContext|debouncedSaveHistory/, + 'StreamManager must not retain a second durable queue or repository write policy'); +assert.match(read('modules/renderer/desktopPushConsumer.js'), /export function createDesktopPushConsumer[\s\S]*unsubscribe[\s\S]*cleanupMessage[\s\S]*dispose/, + 'Desktop push must have an explicit subscription, per-message cleanup and lifecycle owner'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /const desktopPushStates = new Map|onDesktopStatus\(/, + 'StreamManager must not retain Desktop push state or a hidden Desktop status subscription'); +assert.match(read('modules/renderer/streamManager.js'), /createDesktopPushConsumer[\s\S]*desktopPushConsumer\?\.processToken[\s\S]*desktopPushConsumer\?\.dispose/, + 'StreamManager may only consume Desktop push through its explicit owned capability'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /persistProjectedStreamTerminal|finalizeStreamedMessage/, + 'StreamManager must not expose a second durable terminal facade'); +assert.match(read('modules/renderer/mainChatSurfaceAdapter.js'), /persistTerminal: projected => services\.historyPersistence\.commit/, + 'MainChatSurfaceAdapter must own the production durable commit capability'); +assert.match(read('renderer.js'), /createChatHistoryPersistence\(chatRepository\)[\s\S]*historyPersistence,/, + 'the main renderer must compose the durable provider with its real repository'); +assert.match(read('modules/chat/vcpStreamBridge.js'), /disposeOperation[\s\S]*handle\.dispose[\s\S]*operation\.chain/, + 'message-scoped Surface retraction must reach bridge quiescence without a producer terminal'); +assert.match(read('modules/chat/surfaceConversation.js'), /createSurfaceConversation[\s\S]*selectedItemRef[\s\S]*topicIdRef[\s\S]*historyRef[\s\S]*dispose/, + 'internal chat Surfaces must own fixed conversation identity, local history and disposal authority'); +assert.doesNotMatch(read('modules/renderer/streamManager.js'), /(? app\.discoverable !== false\)\.forEach/, + 'Launchpad must expose discoverable internal applications while hiding internal test surfaces'); const eventSource = read('modules/event-listeners.js'); for (const id of [ diff --git a/scripts/fixtures/chat-contract-invalid.mjs b/scripts/fixtures/chat-contract-invalid.mjs new file mode 100644 index 000000000..ec20d2e27 --- /dev/null +++ b/scripts/fixtures/chat-contract-invalid.mjs @@ -0,0 +1,4 @@ +import { assertChatEventContract } from '../../modules/chat/chatEventContract.js'; + +// Deliberately invalid fixture. The parent runner must observe a non-zero exit. +assertChatEventContract({ name: 'chat.stream.completed', terminal: false, durable: true }); diff --git a/scripts/next-delta-shared-baseline.json b/scripts/next-delta-shared-baseline.json index 3dd023096..8cf0e8c39 100644 --- a/scripts/next-delta-shared-baseline.json +++ b/scripts/next-delta-shared-baseline.json @@ -1,19 +1,19 @@ { "modules/chatManager.js": { - "sha256": "22319b39d5d8d13b811166df21b869fc8abb0e02e871e97803b411b489bdce2b", - "reason": "upstream chat core plus separately reviewed M9 ownership fixes" + "sha256": "ba80f4a0d89f51d1445fd203b325bd62ec075056e0a6443813e9b06d662998c2", + "reason": "upstream chat core plus exact Surface stream routing, independent conversation capability, initiating-Surface cleanup, injected send-state capability, durable history authority, and no direct IPC fallback; deletion and topic-summary completion replace immutable selection snapshots through their authority; outgoing attachment draft identity changes on append so a file added during durable persistence is preserved; selection intent persists before watcher/history work and restore waits for topic projection before reporting completion; named ChatManager lifecycle retracts canvas subscriptions, observer, forward timers and selection authority before draining persistence queues; file-history sync now captures selection/topic generations and revalidates the active conversation after its read, protects stream ids captured before the read from terminal/delete races, and reconciles active StreamProjection snapshots after history/DOM replacement without taking over durable persistence" }, "modules/messageRenderer.js": { - "sha256": "8334f93f39e11c0f3b342a8b8546f4e032a9e7627eff966051fc976083c7b41f", - "reason": "upstream message semantics with idempotent scoped-style reuse; Next may not restyle or fork it" + "sha256": "2d1e8858561f46b22d3bb6f59d46b5d0dc4357b0699456762e4f27b4f5992901", + "reason": "upstream message semantics plus explicit Surface-owned renderer factory, root-aware renderer operations, per-Surface style namespace and owned style cleanup, Surface-scoped dynamic style/audio isolation, injected realm/services/capabilities, per-Surface StreamProjection attachment API, owner-scoped context-menu and middle-click callbacks, copy-before-mutate history projection, and avatar-color completion guarded by live selection identity before authority replacement" }, "modules/renderer/streamManager.js": { - "sha256": "42a850a2a53b59ef43200e6edf35af222bac33aa7b110b90fe66e675ec7dfcb2", - "reason": "shared stream core with reviewed scoped-HTML streaming and M9 terminal ownership fixes" + "sha256": "ffeb5f98db5e06c03879cfb3ca2ab274e90c95ff40ea83595961ba352683189b", + "reason": "per-Surface stream projection factory with an explicit StreamProjectionRuntime provider, owning document/window capabilities, attachStreamProjection lifecycle API, injected view authority, transient message models delegated to StreamTransientHistory, owned animation-frame scheduling and pending operation disposal, no repository or durable commit authority, no exported singleton or hidden root side channel, ContentRuntime render models, and owned Desktop push subscription/timer consumer; producer streamOperationId carried into projection and terminal validation; operation-scoped runtime map adapter, retry retirement, display-id projection index, and renderer-local conversation snapshot/reconcile so an active stream can be reprojected after topic history/DOM replacement without becoming durable authority" }, "modules/topicListManager.js": { - "sha256": "69e9bee1fdb65f2e899e28e24df77093888327386a77a756a3e355275ab5c52f", - "reason": "shared topic authority with canonical presentation projection only" + "sha256": "0c613e24341ce273ad5807609b089138edbf098259801df7972d8a0733803607", + "reason": "shared topic authority with canonical presentation projection, mandatory repository access without a direct IPC fallback, explicit injected topic-selection readiness, lifecycle listener owner, ui manager and item-list service capabilities, render-scoped listener cleanup, and disposal of observers, sortable state, context menus, and dynamic topic resources" }, "modules/itemListManager.js": { "sha256": "1a227093cc438de479c1ca8dc9c1abf7acd4dace1bd773c4d4256f10be1bd162", @@ -24,16 +24,16 @@ "reason": "upstream group business surface with retired toolbar binding removed" }, "modules/settingsManager.js": { - "sha256": "2d8e0977399577ba99f635a16aae42c13f914444a7ca88a88656170ee171b34e", - "reason": "upstream/main 3da77f00 settings manager fix plus reviewed Agent settings save settlement after fallible UI projection" + "sha256": "1ac9eee2aa754ee2eda2916a0694c9844fc41f39477fa6c0d4124b3d2eeb2abd", + "reason": "reviewed Agent settings behavior after replacing ambient globalSettings reads with the composition-injected settings authority; saved Agent projection and collapse-state updates replace immutable selection snapshots through the injected selection authority" }, "modules/filterManager.js": { "sha256": "8b632c78f2d0bd525cbe24022b8883c8e6a4c3565352779c5ebc49be9350c17f", "reason": "shared filter authority; canonical menu calls its public API" }, "modules/notificationRenderer.js": { - "sha256": "24b2510275b74eee40a99ec03ed73d256284b1117c785f8733a4094fff13341e", - "reason": "upstream notification rendering semantics" + "sha256": "fbd20f4ecbca87196a2f288ac05df02b5101ce9e9f3ee417ae07da85cd84a3c9", + "reason": "reviewed notification rendering after injected filtering and owner-scoped toast timers and transition listeners replaced ambient lifecycle effects" }, "VCPDistributedServer/frontend-plugin-loader.js": { "sha256": "0987ee41ed76c8b4f0963353c4b387f0b46a6202b9e8516aa02cce27568862ca", diff --git a/scripts/run-chat-contract-invariants.mjs b/scripts/run-chat-contract-invariants.mjs new file mode 100644 index 000000000..35b42f7b4 --- /dev/null +++ b/scripts/run-chat-contract-invariants.mjs @@ -0,0 +1,11 @@ +import assert from 'node:assert/strict'; +import { createStreamSession } from '../modules/chat/streamSession.js'; + +const session = createStreamSession({ sessionId: 'invariant-session', conversationKey: 'invariant-conversation' }); +assert.equal(session.pushChunk('a'), true, 'first chunk must be accepted'); +assert.equal(session.terminal('completed'), true, 'first terminal must be accepted'); +assert.equal(session.terminal('failed'), false, 'second terminal must be rejected'); +assert.equal(session.pushChunk('late'), false, 'late chunk must be rejected'); +assert.equal(session.snapshot.text, 'a', 'late chunk changed stream state'); +assert.equal((await session.dispose('invariant-dispose')).kind, 'completed', 'dispose must preserve accepted terminal'); +console.log('Chat contract runtime invariants passed (single terminal, late-result isolation, quiescent dispose).'); diff --git a/scripts/test-appearance-studio.mjs b/scripts/test-appearance-studio.mjs index 3ac4f0c4c..f7b8ae865 100644 --- a/scripts/test-appearance-studio.mjs +++ b/scripts/test-appearance-studio.mjs @@ -111,9 +111,9 @@ window.uiManager = { } }; window.uiHelperFunctions = { showToastNotification() {}, openModal() {} }; -window.normalizeChatPresentationMode = mode => ['bubble', 'panel', 'immersive'].includes(mode) ? mode : 'bubble'; -window.applyChatPresentationMode = async mode => { - const normalized = window.normalizeChatPresentationMode(mode); +const normalizePresentation = mode => ['bubble', 'panel', 'immersive'].includes(mode) ? mode : 'bubble'; +let applyPresentation = async mode => { + const normalized = normalizePresentation(mode); window.globalSettings.chatPresentationMode = normalized; window.document.body.classList.toggle('chat-presentation-bubble', normalized === 'bubble'); window.document.body.classList.toggle('chat-presentation-panel', normalized === 'panel'); @@ -124,6 +124,18 @@ window.applyChatPresentationMode = async mode => { window.eval(fs.readFileSync('modules/ui-system/lifecycle-scope.js', 'utf8')); window.eval(fs.readFileSync('modules/ui-system/appearance-engine.js', 'utf8')); window.eval(fs.readFileSync('modules/ui-system/appearance-studio.js', 'utf8')); +window.dispatchEvent(new CustomEvent('vcp-appearance-studio-configure', { detail: { + settings: { + get: () => structuredClone(window.globalSettings), + replace: patch => Object.assign(window.globalSettings, structuredClone(patch || {})), + }, + appearance: window.VCPAppearance, + uiManager: window.uiManager, + presentation: { + normalize: normalizePresentation, + apply: (...args) => applyPresentation(...args), + }, +} })); document.dispatchEvent(new CustomEvent('modal-ready', { detail: { modalId: 'globalSettingsModal' } })); const settingsRowHeight = document.getElementById('appearanceSidebarRowHeight'); @@ -446,9 +458,9 @@ const persistedBeforeFailedApply = { homeVisualTagline: window.globalSettings.homeVisualTagline, currentThemeMode: window.globalSettings.currentThemeMode }; -const originalApplyPresentation = window.applyChatPresentationMode; +const originalApplyPresentation = applyPresentation; let failCommittedPresentationApply = false; -window.applyChatPresentationMode = async (...args) => { +applyPresentation = async (...args) => { if (failCommittedPresentationApply) { failCommittedPresentationApply = false; throw new Error('simulated committed presentation failure'); @@ -470,7 +482,7 @@ assert.equal(document.documentElement.dataset.vcpDensity, 'compact', 'failed local apply must restore the in-memory appearance snapshot'); assert.equal(studio.isOpen(), true, 'failed save keeps the studio open for correction'); await studio.close({ rollback: true }); -window.applyChatPresentationMode = originalApplyPresentation; +applyPresentation = originalApplyPresentation; const originalTopTabManager = window.topTabManager; let rejectFirstOverlay; @@ -526,5 +538,7 @@ assert.doesNotMatch( /vchatDynamicWallpaperMenuButton|data-studio-action="wallpaper"/, 'Next Appearance Studio must not depend on a plugin-specific wallpaper adapter' ); -await studio.destroy(); +const disposeReceipt = { promise: Promise.resolve() }; +window.dispatchEvent(new CustomEvent('vcp-appearance-studio-dispose', { detail: disposeReceipt })); +await disposeReceipt.promise; console.log('appearance studio checks passed.'); diff --git a/scripts/test-artifact-plane-invalid.mjs b/scripts/test-artifact-plane-invalid.mjs new file mode 100644 index 000000000..000c90588 --- /dev/null +++ b/scripts/test-artifact-plane-invalid.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const result = spawnSync(process.execPath, ['scripts/check-artifact-plane.mjs'], { + cwd: root, + env: { + ...process.env, + VCPCHAT_NATIVE_RUNTIME_INPUT: path.join(root, 'artifacts', 'deliberately-missing-runtime.exe'), + VCPCHAT_REQUIRE_NATIVE_ARTIFACT: '1', + }, + encoding: 'utf8', +}); +assert.notEqual(result.status, 0, 'missing artifact unexpectedly passed artifact-plane gate'); +assert.match(`${result.stderr}\n${result.stdout}`, /required native artifact is missing/); +console.log('Artifact-plane deliberate invalid runner passed (missing runtime rejected).'); diff --git a/scripts/test-built-artifact-smoke.mjs b/scripts/test-built-artifact-smoke.mjs new file mode 100644 index 000000000..d41287f9d --- /dev/null +++ b/scripts/test-built-artifact-smoke.mjs @@ -0,0 +1,31 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const executable = process.platform === 'win32' ? 'vcp_chat_data_service.exe' : 'vcp_chat_data_service'; +const binary = path.join(root, 'modules/services/chatDataService/bin', `${process.platform}-${process.arch}`, executable); +const originalBinary = fs.existsSync(binary) ? fs.readFileSync(binary) : null; +try { + const build = spawnSync(process.execPath, ['rust_chat_data_service/build-runtime.js'], { + cwd: root, + encoding: 'utf8', + stdio: 'inherit', + // A clean Windows cargo release build is legitimately slower than the + // source-plane gates; classify a timeout as infrastructure failure rather + // than silently accepting a stale binary. + timeout: 600_000, + }); + if (build.error || build.status !== 0) throw build.error || new Error(`build exited with ${build.status}`); + if (!fs.existsSync(binary)) throw new Error(`built runtime missing: ${binary}`); + const help = spawnSync(binary, ['--help'], { cwd: root, encoding: 'utf8', timeout: 20_000 }); + if (help.error || help.status !== 0) throw help.error || new Error(`built runtime --help exited with ${help.status}: ${help.stderr}`); + if (!/Usage|Options|help/i.test(`${help.stdout}\n${help.stderr}`)) throw new Error('built runtime did not expose a help contract'); + console.log(`Built artifact smoke passed (${binary}; --help exit 0).`); +} finally { + // The smoke validates the generated artifact but must not leave a tracked + // release binary modified in a developer worktree. + if (originalBinary) fs.writeFileSync(binary, originalBinary); + else fs.rmSync(binary, { force: true }); +} diff --git a/scripts/test-chat-contract-invalid.mjs b/scripts/test-chat-contract-invalid.mjs new file mode 100644 index 000000000..af4e7535c --- /dev/null +++ b/scripts/test-chat-contract-invalid.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fixture = path.join(root, 'scripts/fixtures/chat-contract-invalid.mjs'); +const result = spawnSync(process.execPath, [fixture], { cwd: root, encoding: 'utf8' }); +assert.notEqual(result.status, 0, 'deliberately invalid contract unexpectedly passed'); +assert.match(`${result.stderr}\n${result.stdout}`, /terminal declaration mismatch/); +const tempContracts = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'vcpchat-contract-invalid-')), 'contracts.json'); +const validContracts = JSON.parse(fs.readFileSync(path.join(root, 'docs/contracts/chat-contracts.json'), 'utf8')); +validContracts[0] = { ...validContracts[0], evidence: ['controlled skipped evidence'] }; +fs.writeFileSync(tempContracts, JSON.stringify(validContracts)); +const statusResult = spawnSync(process.execPath, ['scripts/check-chat-contracts.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_CONTRACTS_INPUT: tempContracts }, + encoding: 'utf8', +}); +assert.notEqual(statusResult.status, 0, 'pass contract with skipped evidence unexpectedly passed'); +assert.match(`${statusResult.stderr}\n${statusResult.stdout}`, /cannot declare pass/); +const malformed = validContracts.map(entry => entry.id === 'facade.main-chat-commands' + ? { ...entry, id: 'INVALID ID', owner: '' } + : entry); +fs.writeFileSync(tempContracts, JSON.stringify(malformed)); +const malformedResult = spawnSync(process.execPath, ['scripts/check-chat-contracts.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_CONTRACTS_INPUT: tempContracts }, + encoding: 'utf8', +}); +assert.notEqual(malformedResult.status, 0, 'malformed contract unexpectedly passed'); +assert.match(`${malformedResult.stderr}\n${malformedResult.stdout}`, /has invalid id|requires a non-empty owner/); +console.log('Chat contract deliberate invalid runner passed (invalid contract failed through a real Node entry).'); diff --git a/scripts/test-chat-evidence-manifest.mjs b/scripts/test-chat-evidence-manifest.mjs new file mode 100644 index 000000000..0f5e29b4e --- /dev/null +++ b/scripts/test-chat-evidence-manifest.mjs @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'vcpchat-evidence-manifest-')); +const main = path.join(temp, 'main.json'); +const apps = path.join(temp, 'apps.json'); +const matrix = path.join(temp, 'matrix.json'); +const soak = path.join(temp, 'soak.json'); +const packaged = path.join(temp, 'packaged.json'); +const output = path.join(temp, 'manifest.json'); +fs.writeFileSync(main, JSON.stringify({ + kind: 'electron-main-chat-sequence', + requiredEdges: '1/1', + coverage: { missingRequiredEdges: [] }, +})); +fs.writeFileSync(apps, JSON.stringify({ kind: 'electron-ui-apps-smoke', passed: 2, total: 2 })); +fs.writeFileSync(matrix, JSON.stringify({ rows: [{ id: 'main', status: 'passed' }] })); +fs.writeFileSync(soak, JSON.stringify({ status: 'manual_observation_required' })); +fs.writeFileSync(packaged, JSON.stringify({ status: 'failed', reason: 'controlled build failure' })); +const run = env => spawnSync(process.execPath, ['scripts/write-chat-evidence-manifest.mjs', output], { + cwd: root, + env: { ...process.env, ...env }, + encoding: 'utf8', +}); +let result = run({ + VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: main, + VCPCHAT_UI_APPS_EVIDENCE_OUTPUT: apps, + VCPCHAT_WINDOWS_MATRIX_EVIDENCE_INPUT: matrix, + VCPCHAT_MANUAL_SOAK_EVIDENCE_INPUT: soak, + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: packaged, +}); +assert.equal(result.status, 0, result.stderr); +let manifest = JSON.parse(fs.readFileSync(output, 'utf8')); +assert.equal(manifest.checks.realElectronSequence, 'pass'); +assert.equal(manifest.checks.realElectronUiApps, 'pass'); +assert.equal(manifest.status, 'manual_required'); +assert.equal(manifest.checks.windowsMatrix, 'pass'); +assert.equal(manifest.checks.manualSoak, 'manual_observation_required'); +assert.equal(manifest.checks.packagedArtifact, 'failed'); +assert.deepEqual(manifest.checks.transcriptSnapshots, [ + 'docs/contracts/snapshots/chat-stream.json', + 'docs/contracts/snapshots/chat-stream-cancel.json', + 'docs/contracts/snapshots/chat-stream-failed.json', + 'docs/contracts/snapshots/chat-stream-discarded.json', +]); +assert.deepEqual(manifest.commands, [ + 'node scripts/check-chat-contracts.mjs', + 'node scripts/run-chat-contract-invariants.mjs', + 'node scripts/test-chat-contract-invalid.mjs', + 'node scripts/test-facade-registry-invalid.mjs', + 'node scripts/test-artifact-plane-invalid.mjs', + 'node scripts/test-packaged-artifact-invalid.mjs', + 'node scripts/check-artifact-plane.mjs', + 'node scripts/test-built-artifact-smoke.mjs', + 'node scripts/test-packaged-artifact-smoke.mjs', + 'node scripts/test-chat-transcript-snapshot.mjs', +]); +for (const command of manifest.commands) { + const script = command.split(' ')[1]; + assert.ok(fs.existsSync(path.join(root, script)), `manifest command script is missing: ${script}`); +} + +result = run({ VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: path.join(temp, 'missing.json') }); +assert.notEqual(result.status, 0); +result = run({ + VCPCHAT_REAL_ELECTRON_STATUS: 'pass', + VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: main, + VCPCHAT_UI_APPS_EVIDENCE_OUTPUT: apps, + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: packaged, +}); +assert.notEqual(result.status, 0, 'forced pass unexpectedly overrode failed packaged evidence'); +assert.match(`${result.stderr}\n${result.stdout}`, /requires packaged artifact status passed/); +result = run({ + VCPCHAT_REAL_ELECTRON_STATUS: 'pass', + VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: main, + VCPCHAT_UI_APPS_EVIDENCE_OUTPUT: apps, +}); +assert.notEqual(result.status, 0, 'forced pass unexpectedly accepted missing release evidence'); +assert.match(`${result.stderr}\n${result.stdout}`, /requires packaged artifact status passed/); +fs.writeFileSync(packaged, JSON.stringify({ status: 'passed' })); +fs.writeFileSync(soak, JSON.stringify({ status: 'passed' })); +result = run({ + VCPCHAT_REAL_ELECTRON_STATUS: 'pass', + VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: main, + VCPCHAT_UI_APPS_EVIDENCE_OUTPUT: apps, + VCPCHAT_WINDOWS_MATRIX_EVIDENCE_INPUT: matrix, + VCPCHAT_MANUAL_SOAK_EVIDENCE_INPUT: soak, + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: packaged, +}); +assert.equal(result.status, 0, result.stderr); +manifest = JSON.parse(fs.readFileSync(output, 'utf8')); +assert.equal(manifest.status, 'pass'); +console.log('Chat evidence manifest validation passed (real evidence paths and invalid paths).'); diff --git a/scripts/test-chat-release-evidence-invalid.mjs b/scripts/test-chat-release-evidence-invalid.mjs new file mode 100644 index 000000000..452e8bc23 --- /dev/null +++ b/scripts/test-chat-release-evidence-invalid.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const result = spawnSync(process.execPath, ['scripts/check-chat-release-evidence.mjs'], { + cwd: root, + env: { ...process.env }, + encoding: 'utf8', +}); +assert.notEqual(result.status, 0, 'release evidence gate unexpectedly passed without evidence inputs'); +assert.match(`${result.stderr}\n${result.stdout}`, /missing .*VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT/); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'vcpchat-release-evidence-')); +const write = (name, value) => { const filename = path.join(temp, name); fs.writeFileSync(filename, JSON.stringify(value)); return filename; }; +const output = path.join(temp, 'manifest.json'); +const env = { + VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT: write('main.json', { kind: 'electron-main-chat-sequence', requiredEdges: '1/1', coverage: { missingRequiredEdges: [] } }), + VCPCHAT_UI_APPS_EVIDENCE_OUTPUT: write('apps.json', { kind: 'electron-ui-apps-smoke', passed: 2, total: 2 }), + VCPCHAT_WINDOWS_MATRIX_EVIDENCE_INPUT: write('matrix.json', { rows: [{ id: 'main', status: 'passed' }] }), + VCPCHAT_MANUAL_SOAK_EVIDENCE_INPUT: write('soak.json', { status: 'passed' }), + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: write('packaged.json', { status: 'passed' }), +}; +const passing = spawnSync(process.execPath, ['scripts/check-chat-release-evidence.mjs', output], { + cwd: root, + env: { ...process.env, ...env }, + encoding: 'utf8', +}); +assert.equal(passing.status, 0, passing.stderr); +assert.equal(JSON.parse(fs.readFileSync(output, 'utf8')).status, 'pass'); +console.log('Chat release evidence runner passed (missing evidence rejected and complete evidence accepted).'); diff --git a/scripts/test-chat-transcript-snapshot.mjs b/scripts/test-chat-transcript-snapshot.mjs new file mode 100644 index 000000000..bf4abd010 --- /dev/null +++ b/scripts/test-chat-transcript-snapshot.mjs @@ -0,0 +1,41 @@ +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createStreamSession } from '../modules/chat/streamSession.js'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const scenarios = [ + { id: 'chat-stream', sessionId: 'snapshot-session', conversationKey: 'snapshot-conversation', prompt: 'say hello', chunks: ['hello ', 'world'], terminal: 'completed' }, + { id: 'chat-stream-cancel', sessionId: 'snapshot-cancel-session', conversationKey: 'snapshot-cancel-conversation', prompt: 'start a reply', chunks: ['partial'], terminal: 'cancelled' }, + { id: 'chat-stream-failed', sessionId: 'snapshot-failed-session', conversationKey: 'snapshot-failed-conversation', prompt: 'trigger an error', chunks: ['partial'], terminal: 'failed' }, + { id: 'chat-stream-discarded', sessionId: 'snapshot-discarded-session', conversationKey: 'snapshot-discarded-conversation', prompt: 'dispose the reply', chunks: ['partial'], terminal: 'discarded' }, +]; +for (const scenario of scenarios) { + const session = createStreamSession({ sessionId: scenario.sessionId, conversationKey: scenario.conversationKey }); + const wire = []; + const statuses = []; + session.subscribe(event => { + wire.push(event.type === 'chunk' ? `chunk:${event.text}` : event.type); + statuses.push(event.type === 'chunk' ? 'streaming' : event.type); + }); + for (const chunk of scenario.chunks) session.pushChunk(chunk); + session.terminal(scenario.terminal); + const snapshot = { + modelVisible: [ + { role: 'user', content: scenario.prompt }, + { role: 'assistant', content: session.snapshot.text, terminal: session.snapshot.terminal.kind }, + ], + chatVisible: { + role: 'assistant', + status: session.snapshot.terminal.kind, + content: session.snapshot.text, + }, + durable: { conversationKey: session.snapshot.conversationKey, terminal: session.snapshot.terminal.kind, text: session.snapshot.text }, + projection: { statuses, chunkCount: session.snapshot.chunkCount, text: session.snapshot.text }, + wire, + }; + const expected = JSON.parse(fs.readFileSync(path.join(root, `docs/contracts/snapshots/${scenario.id}.json`), 'utf8')); + assert.deepEqual(snapshot, expected, `${scenario.id} snapshot changed; review the semantic diff before recording`); +} +console.log('Chat transcript snapshots passed (completed, cancelled, failed and discarded durable/projection/wire transcripts).'); diff --git a/scripts/test-electron-lifecycle-stress.mjs b/scripts/test-electron-lifecycle-stress.mjs index 0f1d9cda9..d0e826940 100644 --- a/scripts/test-electron-lifecycle-stress.mjs +++ b/scripts/test-electron-lifecycle-stress.mjs @@ -23,6 +23,7 @@ const electron = process.platform === 'darwin' ? path.join(root, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron') : path.join(root, 'node_modules', 'electron', 'dist', process.platform === 'win32' ? 'electron.exe' : 'electron'); const timeoutMs = 90_000; +const protocolTimeout = positiveInteger(process.env.VCPCHAT_STRESS_PROTOCOL_TIMEOUT_MS, 120_000); const cycles = positiveInteger(process.env.VCPCHAT_STRESS_CYCLES, 20); const warmupCycles = positiveInteger(process.env.VCPCHAT_STRESS_WARMUP, 3); const checkpointEvery = Math.max(2, positiveInteger(process.env.VCPCHAT_STRESS_CHECKPOINT_EVERY, 5)); @@ -209,6 +210,17 @@ async function collectRendererSnapshot(page, cdp, browserCdp, browser, label) { page.metrics(), browserCdp.send('SystemInfo.getProcessInfo'), page.evaluate(() => ({ + connectedElements: (() => { + let count = 0; + const visit = root => { + for (const element of root.querySelectorAll('*')) { + count += 1; + if (element.shadowRoot) visit(element.shadowRoot); + } + }; + visit(document); + return count; + })(), enhancedSettingsControls: window.VCPUISettingsBridge?.enhancedCount || 0, promptNodes: document.querySelectorAll('#systemPromptContainer *').length, lifecycle: window.VCPLifecycle?.diagnostics?.summary?.() || null, @@ -337,6 +349,52 @@ async function waitForChildExit(child, timeout = 3_000) { }); } +async function waitForRecoveredMainPage(browser, label, deadline = Date.now() + timeoutMs) { + while (Date.now() < deadline) { + for (const candidate of await browser.pages()) { + if (candidate.isClosed() || !candidate.url().includes('main.html')) continue; + try { + // Electron can retain the crashed target briefly. Bound this + // probe so a stale execution context cannot hide the page + // created by main-process recovery. + await candidate.waitForFunction( + () => document.documentElement.dataset.vcpRendererReady === 'true', + { timeout: 1_000 } + ); + return candidate; + } catch { + // The crash target or a loading recovery page is not ready yet. + } + } + await sleep(100); + } + throw new Error(`${label} did not reach renderer readiness after recovery`); +} + +async function terminateChildTree(child) { + if (process.platform === 'win32') { + // Electron's main process can survive SIGTERM on Windows and retain + // GPU/renderer descendants after a renderer crash. Terminate only the + // exact isolated test process tree that this script spawned. + await new Promise(resolve => { + const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { + windowsHide: true, + stdio: 'ignore', + }); + killer.once('error', () => resolve()); + killer.once('exit', () => resolve()); + }); + await waitForChildExit(child); + return; + } + if (child.exitCode !== null || child.signalCode !== null) return; + child.kill('SIGTERM'); + if (!await waitForChildExit(child)) { + child.kill('SIGKILL'); + await waitForChildExit(child); + } +} + function assertNoSustainedLeak(baseline, checkpoints) { const final = checkpoints.at(-1); const heapAllowance = Math.max(10 * 1024 * 1024, baseline.heapUsed * 0.4); @@ -346,6 +404,8 @@ function assertNoSustainedLeak(baseline, checkpoints) { `renderer heap retained too much memory: ${formatBytes(baseline.heapUsed)} -> ${formatBytes(final.heapUsed)}`); assert.ok(final.listeners <= baseline.listeners + listenerAllowance, `event listeners accumulated: ${baseline.listeners} -> ${final.listeners}`); + assert.equal(final.documents, baseline.documents, + `renderer documents accumulated: ${baseline.documents} -> ${final.documents}`); assert.ok(final.pages <= baseline.pages, `WebContents/pages leaked: ${baseline.pages} -> ${final.pages}`); assert.ok(final.processes <= baseline.processes, `Electron processes leaked: ${baseline.processes} -> ${final.processes}`); assert.ok(final.rendererProcesses <= baseline.rendererProcesses, @@ -354,6 +414,8 @@ function assertNoSustainedLeak(baseline, checkpoints) { `VCPUI settings controllers accumulated: ${baseline.enhancedSettingsControls} -> ${final.enhancedSettingsControls}`); assert.equal(final.promptNodes, baseline.promptNodes, `prompt editor DOM accumulated: ${baseline.promptNodes} -> ${final.promptNodes}`); + assert.equal(final.connectedElements, baseline.connectedElements, + `connected document/shadow DOM accumulated: ${baseline.connectedElements} -> ${final.connectedElements}`); assert.ok(final.detachedVcpIcons <= baseline.detachedVcpIcons, `detached VCP icon hosts accumulated: ${baseline.detachedVcpIcons} -> ${final.detachedVcpIcons}`); assert.ok(final.detachedOptions <= baseline.detachedOptions, @@ -724,8 +786,7 @@ async function cycleRendererCrash(page, browser, app, label) { } try { await crashSession.detach(); } catch { /* the crashed target may already be detached */ } - const recoveredPage = await waitForPage(browser, candidate => candidate.url().includes('main.html'), `${label}: recovered main renderer`); - await recoveredPage.waitForFunction(() => document.documentElement.dataset.vcpRendererReady === 'true', { timeout: timeoutMs }); + const recoveredPage = await waitForRecoveredMainPage(browser, `${label}: recovered main renderer`); await recoveredPage.waitForFunction(() => window.topTabManager?.isMounted?.() && window.askNovaController, { timeout: timeoutMs }); await recoveredPage.waitForFunction(expectedId => ( document.querySelector(`[data-view-id="app:${expectedId}"]`) @@ -813,7 +874,10 @@ try { await sleep(150); } } - browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${port}` }); + browser = await puppeteer.connect({ + browserURL: `http://127.0.0.1:${port}`, + protocolTimeout, + }); let page = await waitForPage(browser, candidate => candidate.url().includes('main.html'), 'main renderer'); const trackRendererPage = rendererPage => { rendererPage.on('pageerror', error => rendererErrors.push(error?.stack || String(error))); @@ -893,6 +957,7 @@ try { checkpoint: point.label, heap: formatBytes(point.heapUsed), nodes: point.nodes, + connected: point.connectedElements, listeners: point.listeners, pages: point.pages, processes: point.processes, @@ -915,10 +980,6 @@ try { // app intentionally remains alive after its last window closes, so using // it here would hang the test runner and retain the isolated process tree. try { browser?.disconnect(); } catch { /* debugger may already be gone */ } - if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM'); - if (!await waitForChildExit(child)) { - child.kill('SIGKILL'); - await waitForChildExit(child); - } + await terminateChildTree(child); await fs.rm(appData, { recursive: true, force: true }); } diff --git a/scripts/test-electron-main-chat-sequences.mjs b/scripts/test-electron-main-chat-sequences.mjs index 3316e7058..7f0f63ac4 100644 --- a/scripts/test-electron-main-chat-sequences.mjs +++ b/scripts/test-electron-main-chat-sequences.mjs @@ -19,6 +19,10 @@ const electron = process.platform === 'darwin' ? path.join(root, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron') : path.join(root, 'node_modules', 'electron', 'dist', process.platform === 'win32' ? 'electron.exe' : 'electron'); const timeout = 45_000; +// Electron/CDP can legitimately pause while the renderer is synchronously +// tearing down an embedded Surface. The 300 s default is based on the +// successful Windows run; callers may still lower/raise it explicitly. +const protocolTimeout = positiveInteger(process.env.VCPCHAT_SEQUENCE_PROTOCOL_TIMEOUT_MS, 300_000); const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); const safeFilePart = value => String(value).replace(/[^a-z0-9._-]+/gi, '-').slice(0, 80) || 'unknown'; const requestedUiMode = process.env.VCPCHAT_SEQUENCE_UI_MODE || 'next'; @@ -163,6 +167,37 @@ async function writeAgent(appData, id, topics) { } } +async function terminateChildTree(child) { + if (process.platform === 'win32') { + await new Promise(resolve => { + const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { + windowsHide: true, + stdio: 'ignore', + }); + killer.once('error', resolve); + killer.once('exit', resolve); + }); + await waitForChildExit(child); + return; + } + if (child.exitCode !== null || child.signalCode !== null) return; + child.kill('SIGTERM'); + if (!await waitForChildExit(child)) { + child.kill('SIGKILL'); + await waitForChildExit(child); + } +} + +async function findFilesNamed(directory, fileName) { + const matches = []; + for (const entry of await fs.readdir(directory, { withFileTypes: true })) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) matches.push(...await findFilesNamed(fullPath, fileName)); + else if (entry.isFile() && entry.name === fileName) matches.push(fullPath); + } + return matches; +} + function requestJson(url) { return new Promise((resolve, reject) => http.get(url, response => { let body = ''; @@ -313,6 +348,7 @@ const appData = await fs.mkdtemp(path.join(os.tmpdir(), 'vcpchat-main-sequence-' await Promise.all(identities.map(id => writeAgent(appData, id, topics[id]))); await fs.writeFile(path.join(appData, 'settings.json'), JSON.stringify({ uiMode: requestedUiMode, enableDistributedServer: false, vcpServerUrl: fixture.url, vcpApiKey: 'sequence-key', + assistantAgent: identities[0], assistantEnabled: false, }), 'utf8'); const debugPort = await freePort(); const stderr = { value: '' }; @@ -330,7 +366,10 @@ try { if (child.exitCode !== null) throw new Error(`Electron exited: ${stderr.value}`); try { await requestJson(`http://127.0.0.1:${debugPort}/json/version`); break; } catch { await sleep(100); } } - browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${debugPort}` }); + browser = await puppeteer.connect({ + browserURL: `http://127.0.0.1:${debugPort}`, + protocolTimeout, + }); let page; while (Date.now() < deadline && !page) { page = (await browser.pages()).find(candidate => candidate.url().includes('main.html')); @@ -352,11 +391,64 @@ try { }; trackPage(page); await page.waitForFunction(() => document.documentElement.dataset.vcpRendererReady === 'true', { timeout }); + const providerBoundary = await page.evaluate(async () => { + const [{ chatManager }, { createMessageRenderer }, { createStreamProjection }] = await Promise.all([ + import('./modules/chatManager.js'), + import('./modules/messageRenderer.js'), + import('./modules/renderer/streamManager.js'), + ]); + return { + noGlobals: !('chatManager' in window) + && !('messageRenderer' in window) + && !('streamManager' in window), + chatProvider: typeof chatManager.sendMessage === 'function', + rendererProvider: typeof createMessageRenderer === 'function', + streamProvider: typeof createStreamProjection === 'function', + }; + }); + assert.deepEqual(providerBoundary, { + noGlobals: true, + chatProvider: true, + rendererProvider: true, + streamProvider: true, + }, 'main renderer must use explicit providers without compatibility globals'); + const stateFacadeBoundary = await page.evaluate(() => { + const descriptor = Object.getOwnPropertyDescriptor(window, 'VCPMainChatState'); + const snapshot = window.VCPMainChatState.snapshot(); + return { + frozen: Object.isFrozen(window.VCPMainChatState), + writable: descriptor?.writable, + configurable: descriptor?.configurable, + snapshotFrozen: Object.isFrozen(snapshot), + selectedFrozen: Object.isFrozen(snapshot.selectedItem), + selectedConfigFrozen: snapshot.selectedItem?.config == null || Object.isFrozen(snapshot.selectedItem.config), + historyFrozen: Object.isFrozen(snapshot.history), + historyEntryFrozen: snapshot.history.length === 0 || Object.isFrozen(snapshot.history[0]), + hasMutationRef: 'selectedItemRef' in window.VCPMainChatState || 'topicIdRef' in window.VCPMainChatState, + hasHistoryRef: 'historyRef' in window.VCPMainChatState, + }; + }); + assert.deepEqual(stateFacadeBoundary, { + frozen: true, + writable: false, + configurable: false, + snapshotFrozen: true, + selectedFrozen: true, + selectedConfigFrozen: true, + historyFrozen: true, + historyEntryFrozen: true, + hasMutationRef: false, + hasHistoryRef: false, + }, 'VCPMainChatState must remain a frozen, non-replaceable read-only plugin facade'); await page.waitForFunction(ids => ids.every(id => document.querySelector(`#agentList > [data-item-id="${id}"][data-item-type="agent"]`)), { timeout }, identities); const click = async selector => page.evaluate(value => document.querySelector(value)?.click(), selector); const waitForRecoveredMainPage = async () => { - const recoveryDeadline = Date.now() + timeout; + // Recovery can legitimately spend tens of seconds rebuilding the + // message projection after a crash while history is rendered. Keep + // the normal action timeout strict, but give renderer recovery its + // own bounded quiescence window. + const recoveryDeadline = Date.now() + Math.max(timeout, 90_000); while (Date.now() < recoveryDeadline) { for (const candidate of await browser.pages()) { if (candidate.isClosed() || !candidate.url().includes('main.html')) continue; @@ -382,15 +474,15 @@ try { const waitState = async (id, topicId) => { try { await page.waitForFunction((expectedId, expectedTopic) => ( - window.currentSelectedItem?.id === expectedId - && window.currentTopicId === expectedTopic + window.VCPMainChatState?.snapshot?.()?.selectedItem?.id === expectedId + && window.VCPMainChatState?.snapshot?.()?.topicId === expectedTopic && document.querySelector(`#agentList > [data-item-id="${expectedId}"][data-item-type="agent"].active`) && document.querySelector(`[data-topic-id="${expectedTopic}"].active`) ), { timeout: 8_000 }, id, topicId); } catch (error) { const actual = await page.evaluate(() => ({ - id: window.currentSelectedItem?.id, - topicId: window.currentTopicId, + id: window.VCPMainChatState?.snapshot?.()?.selectedItem?.id, + topicId: window.VCPMainChatState?.snapshot?.()?.topicId, activeItems: [...document.querySelectorAll('#agentList > .active')].map(node => node.dataset.itemId), activeTopics: [...document.querySelectorAll('.topic-item.active')].map(node => node.dataset.topicId), })); @@ -416,7 +508,7 @@ try { }; const waitForStreamQuiescence = async () => { await page.waitForFunction(() => { - const streams = window.streamManager?.getDiagnostics?.(); + const streams = window.VCPLifecycleInspector?.snapshot?.().streams; return !streams || ( streams.activeMessageId === null @@ -425,7 +517,7 @@ try { && streams.pendingFinalizations === 0 ); }, { timeout: 8_000 }); - return page.evaluate(() => window.streamManager?.getDiagnostics?.() || null); + return page.evaluate(() => window.VCPLifecycleInspector?.snapshot?.().streams || null); }; const driver = { async selectAgent(id, topicId) { @@ -440,12 +532,12 @@ try { await waitState(last, topicId); }, async selectTopic(topicId) { - const id = await page.evaluate(() => window.currentSelectedItem.id); + const id = await page.evaluate(() => window.VCPMainChatState.snapshot().selectedItem.id); await click(`[data-topic-id="${topicId}"][data-item-id="${id}"]`); await waitState(id, topicId); }, async raceTopics(first, last) { - const id = await page.evaluate(() => window.currentSelectedItem.id); + const id = await page.evaluate(() => window.VCPMainChatState.snapshot().selectedItem.id); await page.evaluate(({ firstId, lastId, itemId }) => { document.querySelector(`[data-topic-id="${firstId}"][data-item-id="${itemId}"]`)?.click(); document.querySelector(`[data-topic-id="${lastId}"][data-item-id="${itemId}"]`)?.click(); @@ -463,14 +555,14 @@ try { const requestDeadline = Date.now() + 5_000; while (fixture.requests.length === before && Date.now() < requestDeadline) await sleep(10); assert.ok(fixture.requests.length > before, 'stream request did not reach the controlled VCP fixture'); - const id = await page.evaluate(() => window.currentSelectedItem.id); + const id = await page.evaluate(() => window.VCPMainChatState.snapshot().selectedItem.id); await click(`[data-topic-id="${targetTopic}"][data-item-id="${id}"]`); await waitState(id, targetTopic); await sleep(500); }, async concurrentStreamsReverse(targetTopic, nonce) { - const itemId = await page.evaluate(() => window.currentSelectedItem.id); - const sourceTopic = await page.evaluate(() => window.currentTopicId); + const itemId = await page.evaluate(() => window.VCPMainChatState.snapshot().selectedItem.id); + const sourceTopic = await page.evaluate(() => window.VCPMainChatState.snapshot().topicId); const firstKey = `first-${nonce}`; const secondKey = `second-${nonce}`; const sendHeld = key => page.evaluate(holdKey => { @@ -536,12 +628,12 @@ try { }, async createDeleteTopicRoundtrip(expectedTopic) { const item = await page.evaluate(() => ({ - id: window.currentSelectedItem.id, - type: window.currentSelectedItem.type, + id: window.VCPMainChatState.snapshot().selectedItem.id, + type: window.VCPMainChatState.snapshot().selectedItem.type, })); const createdTopicId = await page.evaluate(async ({ itemId, itemType }) => { const before = new Set((await window.chatAPI.getAgentTopics(itemId)).map(topic => topic.id)); - await window.chatManager.createNewTopicForItem(itemId, itemType); + await (await import('./modules/chatManager.js')).chatManager.createNewTopicForItem(itemId, itemType); const after = await window.chatAPI.getAgentTopics(itemId); return after.find(topic => !before.has(topic.id))?.id || null; }, { itemId: item.id, itemType: item.type }); @@ -551,7 +643,7 @@ try { const deletion = await page.evaluate(async ({ itemId, itemType, topicId }) => { const result = await window.chatAPI.deleteTopic(itemId, topicId); if (!result?.success) return result; - await window.chatManager.handleTopicDeletion(result.remainingTopics, { id: itemId, type: itemType }); + await (await import('./modules/chatManager.js')).chatManager.handleTopicDeletion(result.remainingTopics, { id: itemId, type: itemType, topicId, fallbackTopicId: 'a-two' }); return result; }, { itemId: item.id, itemType: item.type, topicId: createdTopicId }); assert.equal(deletion?.success, true, `topic deletion roundtrip failed: ${JSON.stringify(deletion)}`); @@ -576,8 +668,6 @@ try { } catch (error) { const state = await page.evaluate(() => ({ mode: document.getElementById('sendMessageBtn')?.dataset.mode, - activeStreamId: window.streamManager?.getActiveStreamingMessageId?.(), - activeContext: window.streamManager?.getActiveStreamingContext?.(), streamingDom: [...document.querySelectorAll('.message-item.streaming')].map(node => node.dataset.messageId), })); throw new Error(`cancel did not settle: ${JSON.stringify(state)}`, { cause: error }); @@ -594,7 +684,19 @@ try { }, kind); const requestDeadline = Date.now() + 5_000; while (fixture.requests.length === before && Date.now() < requestDeadline) await sleep(10); - assert.ok(fixture.requests.length > before, `${kind} request did not reach the controlled VCP fixture`); + if (fixture.requests.length === before) { + const state = await page.evaluate(() => ({ + selected: window.VCPMainChatState?.snapshot?.()?.selectedItem?.id || null, + topicId: window.VCPMainChatState?.snapshot?.()?.topicId || null, + inputDisabled: document.getElementById('messageInput')?.disabled, + sendDisabled: document.getElementById('sendMessageBtn')?.disabled, + sendMode: document.getElementById('sendMessageBtn')?.dataset.mode, + pending: (window.currentChatHistory || []).filter(message => message?.isThinking || message?.isPendingStream).map(message => message.id), + streamingDom: [...document.querySelectorAll('#chatMessages .message-item.streaming')].map(node => node.dataset.messageId), + recentAssistant: (window.currentChatHistory || []).filter(message => message?.role === 'assistant').slice(-3).map(message => ({ id: message.id, thinking: message.isThinking, pending: message.isPendingStream })), + })); + assert.fail(`${kind} request did not reach the controlled VCP fixture: ${JSON.stringify(state)}`); + } await page.waitForFunction(() => ( document.getElementById('sendMessageBtn')?.dataset.mode !== 'interrupt' && document.querySelectorAll('.message-item.streaming').length === 0 @@ -602,8 +704,8 @@ try { }, async recoverDuringHeldStream(kind, nonce) { const expected = await page.evaluate(() => ({ - id: window.currentSelectedItem?.id, - topicId: window.currentTopicId, + id: window.VCPMainChatState?.snapshot?.()?.selectedItem?.id, + topicId: window.VCPMainChatState?.snapshot?.()?.topicId, })); assert.ok(expected.id && expected.topicId, `${kind}: no selected conversation`); const key = `${kind}-${nonce}`; @@ -644,7 +746,6 @@ try { await page.waitForFunction(() => ( document.getElementById('sendMessageBtn')?.dataset.mode !== 'interrupt' && document.querySelectorAll('.message-item.streaming').length === 0 - && !window.streamManager?.getActiveStreamingMessageId?.() ), { timeout: 8_000 }); const historySource = await fs.readFile( @@ -688,8 +789,8 @@ try { }, }; const observe = () => page.evaluate(() => ({ - identity: window.currentSelectedItem?.id || null, - topicId: window.currentTopicId || null, + identity: window.VCPMainChatState?.snapshot?.()?.selectedItem?.id || null, + topicId: window.VCPMainChatState?.snapshot?.()?.topicId || null, activeItems: [...document.querySelectorAll('#agentList > [data-item-id][data-item-type].active')].map(node => node.dataset.itemId), activeTopics: [...document.querySelectorAll('.topic-item.active')].map(node => node.dataset.topicId), rendererReady: document.documentElement.dataset.vcpRendererReady, @@ -697,7 +798,7 @@ try { settingsActive: document.getElementById('globalSettingsModal')?.classList.contains('active') === true, mainConnected: Boolean(document.querySelector('.container')?.isConnected && document.querySelector('.main-content')?.isConnected), streamingMessages: document.querySelectorAll('.message-item.streaming').length, - streams: window.streamManager?.getDiagnostics?.() || null, + streams: window.VCPLifecycleInspector?.snapshot?.().streams || null, lifecycle: window.VCPLifecycle?.diagnostics?.summary?.() || null, })); const collectResourceCheckpoint = async label => { @@ -777,10 +878,15 @@ try { const itemId = identities[0]; const topicId = topics[itemId][0]; await click(`#agentList > [data-item-id="${itemId}"][data-item-type="agent"]`); - await page.waitForFunction(expectedId => window.currentSelectedItem?.id === expectedId, { timeout: 8_000 }, itemId); - const activeTopic = await page.evaluate(() => window.currentTopicId); + await page.waitForFunction(expectedId => window.VCPMainChatState?.snapshot?.()?.selectedItem?.id === expectedId, { timeout: 8_000 }, itemId); + const activeTopic = await page.evaluate(() => window.VCPMainChatState.snapshot().topicId); if (activeTopic !== topicId) { - await page.evaluate(expectedTopicId => window.chatManager.selectTopic(expectedTopicId), topicId); + // Selecting an item starts the asynchronous topic-list load. Do + // not issue the imperative selectTopic command until its real DOM + // consumer exists; otherwise a reset immediately after a reload + // can select a topic before the list manager has mounted it. + await page.waitForSelector(`[data-topic-id="${topicId}"]`, { timeout: 8_000 }); + await page.evaluate(async expectedTopicId => (await import('./modules/chatManager.js')).chatManager.selectTopic(expectedTopicId), topicId); } await waitState(itemId, topicId); }; @@ -799,12 +905,313 @@ try { } }, topics); await ensureInitialConversation(); - await page.evaluate(({ itemId, topicId }) => ( - window.chatManager.loadChatHistory(itemId, 'agent', topicId) + await page.evaluate(async ({ itemId, topicId }) => ( + (await import('./modules/chatManager.js')).chatManager.loadChatHistory(itemId, 'agent', topicId) ), { itemId: identities[0], topicId: topics[identities[0]][0] }); - await page.evaluate(() => window.streamManager?.cleanupTransientState?.()); }; await ensureInitialConversation(); + // C6 real VCP fixture matrix: the independent interactive surface must + // use the production preload/VCP path, expose a real terminal state, and + // retain retry capability after a controlled failure. + await page.evaluate(() => window.topTabManager.openInternalApp('standalone-chat-compose')); + await page.waitForSelector('.vcp-interactive-chat__composer textarea', { timeout }); + const standaloneRoot = '.vcp-interactive-chat'; + const sendStandalone = async (content) => { + const before = fixture.requests.length; + await page.evaluate(({ root, value }) => { + const form = document.querySelector(`${root} form`); + const input = form?.querySelector('textarea'); + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + form?.requestSubmit(); + }, { root: standaloneRoot, value: content }); + const requestDeadline = Date.now() + 8_000; + while (fixture.requests.length <= before && Date.now() < requestDeadline) await sleep(10); + assert.ok(fixture.requests.length > before, `standalone VCP request did not reach fixture: ${content}`); + }; + const waitForAuxiliaryPage = async (fragment, { exclude = null } = {}) => { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + const candidate = (await browser.pages()).find(entry => entry !== exclude && !entry.isClosed() && entry.url().includes(fragment)); + if (candidate) { + trackPage(candidate); + try { await candidate.waitForSelector('#messageInput:not([disabled])', { timeout: 1_500 }); return candidate; } catch { /* stale/crashed candidate */ } + } + await sleep(50); + } + throw new Error(`Auxiliary Electron page did not open: ${fragment}`); + }; + const waitForAuxiliaryClose = async auxiliaryPage => { + const deadline = Date.now() + timeout; + while (!auxiliaryPage.isClosed() && Date.now() < deadline) await sleep(25); + assert.equal(auxiliaryPage.isClosed(), true, `Auxiliary Electron page did not close: ${auxiliaryPage.url()}`); + }; + const runAuxiliaryWindowScenario = async ({ fragment, open, closeSelector, label }) => { + await open(); + let auxiliaryPage = await waitForAuxiliaryPage(fragment); + await auxiliaryPage.type('#messageInput', `${label}-success`); + await auxiliaryPage.click('#sendMessageBtn'); + await auxiliaryPage.waitForFunction(() => ( + !document.querySelector('#messageInput')?.disabled + && [...document.querySelectorAll('.message-item.assistant .md-content')] + .some(node => /fixture\s+stream\s+complete/.test(node.textContent || '')) + ), { timeout }); + await auxiliaryPage.click(closeSelector); + await waitForAuxiliaryClose(auxiliaryPage); + + await open(); + auxiliaryPage = await waitForAuxiliaryPage(fragment); + const holdKey = `${label}-close-${Date.now()}`; + await auxiliaryPage.type('#messageInput', `sequence-hold-${holdKey}`); + await auxiliaryPage.click('#sendMessageBtn'); + await fixture.waitPending(holdKey); + await auxiliaryPage.click(closeSelector); + fixture.release(holdKey); + await waitForAuxiliaryClose(auxiliaryPage); + + await open(); + auxiliaryPage = await waitForAuxiliaryPage(fragment); + const reloadKey = `${label}-reload-${Date.now()}`; + await auxiliaryPage.type('#messageInput', `sequence-hold-${reloadKey}`); + await auxiliaryPage.click('#sendMessageBtn'); + await fixture.waitPending(reloadKey); + await auxiliaryPage.reload({ waitUntil: 'domcontentloaded', timeout: 20_000 }); + fixture.release(reloadKey); + await auxiliaryPage.waitForSelector('#messageInput:not([disabled])', { timeout }); + await auxiliaryPage.waitForFunction(() => ( + !document.querySelector('.message-item.is-thinking, .message-item.streaming') + && !document.querySelector('#messageInput')?.disabled + ), { timeout }); + await auxiliaryPage.close(); + await waitForAuxiliaryClose(auxiliaryPage); + + if (process.env.VCPCHAT_AUX_CRASH_MATRIX !== '1') return; + await open(); + auxiliaryPage = await waitForAuxiliaryPage(fragment); + const crashKey = `${label}-crash-${Date.now()}`; + await auxiliaryPage.type('#messageInput', `sequence-hold-${crashKey}`); + await auxiliaryPage.click('#sendMessageBtn'); + await fixture.waitPending(crashKey); + const crashSession = await auxiliaryPage.createCDPSession(); + try { await crashSession.send('Page.crash'); } catch (error) { + if (!/Target closed|Session closed|crash/i.test(String(error?.message || error))) throw error; + } + try { await crashSession.detach(); } catch { /* crashed target */ } + fixture.release(crashKey); + const crashedPage = auxiliaryPage; + await sleep(1_000); + await open(); + auxiliaryPage = await waitForAuxiliaryPage(fragment, { exclude: crashedPage }); + await auxiliaryPage.waitForFunction(() => ( + !document.querySelector('.message-item.is-thinking, .message-item.streaming') + && !document.querySelector('#messageInput')?.disabled + ), { timeout }); + await auxiliaryPage.close(); + await waitForAuxiliaryClose(auxiliaryPage); + }; + await sendStandalone('standalone-success'); + try { + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent === '已发送', { timeout }); + } catch (error) { + const independentState = await page.evaluate(() => ({ + status: document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent || '', + busy: document.querySelector('.vcp-interactive-chat__composer')?.getAttribute('aria-busy') || null, + text: document.querySelector('.vcp-interactive-chat .vcp-standalone-chat__messages')?.textContent || '', + })); + throw new Error(`Independent chat did not settle: ${JSON.stringify(independentState)}`, { cause: error }); + } + await page.waitForFunction(() => { + const root = document.querySelector('.vcp-interactive-chat .vcp-standalone-chat__messages'); + return Boolean(root?.querySelector('.message-item') && root.textContent.includes('fixture')); + }, { timeout }); + // The independent Surface owns its projection after registration. Changing + // the main-window conversation must not revoke its DOM or terminal rights. + const independentSwitchKey = `independent-switch-${Date.now()}`; + await sendStandalone(`sequence-hold-${independentSwitchKey}`); + await fixture.waitPending(independentSwitchKey); + await driver.selectTopic(topics[identities[0]][1]); + fixture.release(independentSwitchKey); + await page.waitForFunction(expected => { + const surface = document.querySelector('.vcp-interactive-chat'); + return surface?.querySelector('[role="status"]')?.textContent === '已发送' + && (surface.querySelector('.vcp-standalone-chat__messages')?.textContent || '').includes(expected); + }, { timeout }, independentSwitchKey); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat__composer')?.hasAttribute('aria-busy'), { timeout }); + await driver.selectTopic(topics[identities[0]][0]); + + // Main and independent operations may coexist on different topics. The + // independent cancel control must cancel only its captured operation. + const isolatedIndependentKey = `isolated-independent-${Date.now()}`; + await sendStandalone(`sequence-hold-${isolatedIndependentKey}`); + await fixture.waitPending(isolatedIndependentKey); + await driver.selectTopic(topics[identities[0]][1]); + const isolatedMainKey = `isolated-main-${Date.now()}`; + await page.evaluate(holdKey => { + const input = document.getElementById('messageInput'); + input.value = `sequence-hold-${holdKey}`; + input.dispatchEvent(new Event('input', { bubbles: true })); + document.getElementById('sendMessageBtn').click(); + }, isolatedMainKey); + await fixture.waitPending(isolatedMainKey); + await page.click('.vcp-interactive-chat__composer [data-action="cancel"]'); + if (fixture.pending.has(isolatedIndependentKey)) fixture.release(isolatedIndependentKey); + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent === '已取消', { timeout }); + assert.equal(fixture.pending.has(isolatedMainKey), true, 'independent cancel aborted the main-window request'); + assert.equal(await page.$eval('#sendMessageBtn', button => button.dataset.mode), 'interrupt', + 'independent cancel cleared the main-window operation state'); + fixture.release(isolatedMainKey); + await waitForStreamQuiescence(); + await driver.selectTopic(topics[identities[0]][0]); + + await sendStandalone('sequence-fail'); + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent?.includes('发送失败'), { timeout }); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat__composer')?.hasAttribute('aria-busy'), { timeout }); + const standaloneFailureState = await page.evaluate(() => ({ + status: document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent || '', + root: document.querySelector('.vcp-interactive-chat .vcp-standalone-chat__messages')?.dataset.chatSurface || '', + independent: document.querySelector('.vcp-interactive-chat .vcp-standalone-chat__messages') !== document.getElementById('chatMessages'), + })); + assert.equal(standaloneFailureState.root, 'interactive'); + assert.equal(standaloneFailureState.independent, true); + // Closing an interactive surface while its real operation is pending must + // reach quiescence before the owner is gone; the late stream terminal must + // not recreate or mutate the detached surface. + await sendStandalone(`sequence-hold-close-${Date.now()}`); + const closeKey = [...fixture.pending.keys()].find(key => key.startsWith('close-')); + assert.ok(closeKey, 'close-while-pending fixture request was not observed'); + await page.evaluate(() => { + window.topTabManager.closeView('app:standalone-chat-compose'); + window.topTabManager.closeView('app:standalone-chat-compose'); + }); + fixture.release(closeKey); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat'), { timeout }); + await sleep(200); + assert.equal(await page.$('.vcp-interactive-chat'), null, 'late terminal recreated disposed interactive surface'); + await page.evaluate(() => window.topTabManager.openInternalApp('standalone-chat-compose')); + await page.waitForSelector('.vcp-interactive-chat__composer textarea', { timeout }); + const independentCancelKey = `independent-${Date.now()}`; + await sendStandalone(`sequence-hold-${independentCancelKey}`); + await fixture.waitPending(independentCancelKey); + await page.click('.vcp-interactive-chat__composer [data-action="cancel"]'); + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent === '已取消', { timeout }); + fixture.release(independentCancelKey); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat__composer')?.hasAttribute('aria-busy'), { timeout }); + await sendStandalone('sequence-disconnect'); + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer [role="status"]')?.textContent?.includes('发送失败'), { timeout }); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat__composer')?.hasAttribute('aria-busy'), { timeout }); + await page.evaluate(() => { + const input = document.querySelector('.vcp-interactive-chat__composer textarea'); + input.value = 'standalone-retry'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval('.vcp-interactive-chat__composer textarea', input => input.disabled), false, 'disconnect left standalone composer locked'); + await page.evaluate(() => { + window.topTabManager.closeView('app:standalone-chat-compose'); + window.topTabManager.closeView('app:standalone-chat-compose'); + }); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat'), { timeout }); + const flowlockAgentConfigPath = path.join(appData, 'Agents', identities[0], 'config.json'); + const flowlockAgentConfig = JSON.parse(await fs.readFile(flowlockAgentConfigPath, 'utf8')); + await fs.writeFile(flowlockAgentConfigPath, JSON.stringify({ ...flowlockAgentConfig, streamOutput: false }), 'utf8'); + const flowlockMessageId = `flowlock-electron-${Date.now()}`; + const flowlockRequestCount = fixture.requests.length; + await page.evaluate(async ({ agentId, topicId, messageId }) => { + await window.flowlockManager.continueWritingForContext({ + agentId, + topicId, + messageId, + prompt: 'Flowlock Electron terminal evidence', + }); + }, { agentId: identities[0], topicId: topics[identities[0]][0], messageId: flowlockMessageId }); + assert.equal(fixture.requests.length, flowlockRequestCount + 1, 'Flowlock non-stream request did not reach the real VCP fixture'); + assert.equal(fixture.requests.at(-1)?.stream, false, 'Flowlock terminal evidence must exercise the non-stream producer path'); + const flowlockHistoryPath = path.join(appData, 'UserData', identities[0], 'topics', topics[identities[0]][0], 'history.json'); + const flowlockHistory = JSON.parse(await fs.readFile(flowlockHistoryPath, 'utf8')); + assert.equal(flowlockHistory.some(message => message?.id === flowlockMessageId || message?.isThinking), false, + 'Flowlock terminal commit retained its thinking placeholder'); + assert.equal(flowlockHistory.filter(message => message?.content === 'fixture response').length, 1, + 'Flowlock non-stream completion must commit exactly one assistant terminal'); + await fs.writeFile(flowlockAgentConfigPath, JSON.stringify(flowlockAgentConfig), 'utf8'); + await runAuxiliaryWindowScenario({ + fragment: 'Voicechatmodules/voicechat.html', + open: () => page.evaluate(agentId => window.chatAPI.openVoiceChatWindow({ agentId }), identities[0]), + closeSelector: '#close-btn-voicechat', + label: 'voice', + }); + await runAuxiliaryWindowScenario({ + fragment: 'rust_assistant_engine/ui/assistant.html', + open: () => page.evaluate(() => window.chatAPI.assistantAction('open')), + closeSelector: '#close-btn-assistant', + label: 'assistant', + }); + + // D7: both auxiliary surfaces must be able to own real operations at the + // same time. Opening and settling them independently catches accidental + // singleton renderer/stream state, while the final close verifies that + // each window reaches its own quiescent terminal before teardown. + // Open through the main renderer serially (the two IPC open commands share + // one composition root), then run their actual stream operations in + // parallel below. + await page.evaluate(agentId => window.chatAPI.openVoiceChatWindow({ agentId }), identities[0]); + await page.evaluate(() => window.chatAPI.assistantAction('open')); + const [concurrentVoicePage, concurrentAssistantPage] = await Promise.all([ + waitForAuxiliaryPage('Voicechatmodules/voicechat.html'), + waitForAuxiliaryPage('rust_assistant_engine/ui/assistant.html'), + ]); + // The input is enabled before the preload data callback finishes in the + // auxiliary windows. Wait for each window's real agent binding before + // sending, otherwise the context filter quite correctly rejects events. + await concurrentAssistantPage.waitForFunction(() => { + const avatar = document.querySelector('#agentAvatar'); + return Boolean(avatar?.getAttribute('src')) + && document.querySelector('#currentChatAgentName')?.textContent !== '划词助手'; + }, { timeout }); + await concurrentVoicePage.waitForFunction(() => ( + document.querySelector('#currentChatAgentName')?.textContent + && document.querySelector('#currentChatAgentName')?.textContent !== '语音聊天' + ), { timeout }); + await Promise.all([ + (async () => { + await concurrentVoicePage.type('#messageInput', 'voice-concurrent'); + await concurrentVoicePage.click('#sendMessageBtn'); + await concurrentVoicePage.waitForFunction(() => ( + !document.querySelector('#messageInput')?.disabled + && [...document.querySelectorAll('.message-item.assistant .md-content')] + .some(node => /fixture\s+stream\s+complete/.test(node.textContent || '')) + ), { timeout }); + })(), + (async () => { + await concurrentAssistantPage.type('#messageInput', 'assistant-concurrent'); + await concurrentAssistantPage.click('#sendMessageBtn'); + await concurrentAssistantPage.waitForFunction(() => ( + !document.querySelector('#messageInput')?.disabled + && [...document.querySelectorAll('.message-item.assistant .md-content')] + .some(node => /fixture\s+stream\s+complete/.test(node.textContent || '')) + ), { timeout }); + })(), + ]); + await concurrentVoicePage.click('#close-btn-voicechat'); + await concurrentAssistantPage.click('#close-btn-assistant'); + await Promise.all([ + waitForAuxiliaryClose(concurrentVoicePage), + waitForAuxiliaryClose(concurrentAssistantPage), + ]); + const allHistoryFiles = await findFilesNamed(path.join(appData, 'UserData'), 'history.json'); + const parsedHistories = await Promise.all(allHistoryFiles + .map(async file => ({ file, history: JSON.parse(await fs.readFile(file, 'utf8')) }))); + const auxiliaryHistories = parsedHistories.filter(({ history }) => history.some(message => ( + message?.content === 'voice-success' || message?.content === 'assistant-success' + ))); + const durableAuxiliaryLabels = new Set(auxiliaryHistories.flatMap(({ history }) => history + .map(message => message?.content) + .filter(content => content === 'voice-success' || content === 'assistant-success'))); + assert.deepEqual([...durableAuxiliaryLabels].sort(), ['assistant-success', 'voice-success'], + `Voice and Rust Assistant must each commit a real auxiliary history; histories=${JSON.stringify(parsedHistories.map(({ file, history }) => ({ file, messages: history.map(message => ({ role: message?.role, content: message?.content, thinking: message?.isThinking })) })))}; console=${JSON.stringify(consoleMessages.filter(message => message.type === 'error').slice(-20))}`); + for (const { file, history } of auxiliaryHistories) { + assert.equal(history.some(message => message?.isThinking), false, `Auxiliary close persisted a thinking placeholder: ${file}`); + assert.equal(history.some(message => /fixture\s+stream\s+complete/.test(message?.content || '')), true, + `Auxiliary success terminal was not durably saved: ${file}`); + } await warmRendererLifecycleBaseline(); await driver.sendFault('fail'); await resetFixtureConversationState(); @@ -967,15 +1374,24 @@ try { throw error; } const coverageReport = sequenceCoverage.report(); + if (process.env.VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT) { + await fs.writeFile(process.env.VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT, `${JSON.stringify({ + schemaVersion: 1, + kind: 'electron-main-chat-sequence', + mode: requestedUiMode, + seed, + runs: runCount, + actions: totalActions, + requests: fixture.requests.length, + requiredEdges: `${coverageReport.passedRequiredEdges.length}/${coverageReport.requiredEdges.length}`, + coverage: coverageReport, + }, null, 2)}\n`, 'utf8'); + } console.log(`Main-chat Electron sequence passed: mode=${requestedUiMode}, seed=${seed}, runs=${runCount}, actions=${totalActions}, VCP requests=${fixture.requests.length}`); console.log(`Sequence coverage: actions=${Object.keys(coverageReport.actions).length}, pairs=${Object.keys(coverageReport.actionPairs).length}, transitions=${Object.keys(coverageReport.transitions).length}, faults=${Object.keys(coverageReport.faults).length}, required=${coverageReport.passedRequiredEdges.length}/${coverageReport.requiredEdges.length}`); } finally { try { await browser?.disconnect(); } catch { /* noop */ } - if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM'); - if (!await waitForChildExit(child)) { - child.kill('SIGKILL'); - await waitForChildExit(child); - } + await terminateChildTree(child); await fixture.close(); await fs.rm(appData, { recursive: true, force: true }); } diff --git a/scripts/test-electron-manual-soak.mjs b/scripts/test-electron-manual-soak.mjs new file mode 100644 index 000000000..fb080b33e --- /dev/null +++ b/scripts/test-electron-manual-soak.mjs @@ -0,0 +1,210 @@ +// Interactive manual-soak harness for vD7 evidence. +// This tool observes a real Electron renderer but never marks the product as +// passed: an operator must record the checklist result after the session. + +import { spawn } from 'node:child_process'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import puppeteer from 'puppeteer'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const electron = process.platform === 'darwin' + ? path.join(root, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron') + : path.join(root, 'node_modules', 'electron', 'dist', process.platform === 'win32' ? 'electron.exe' : 'electron'); +const minutes = positiveNumber(process.env.VCPCHAT_MANUAL_SOAK_MINUTES, 30); +const intervalMs = positiveNumber(process.env.VCPCHAT_MANUAL_SOAK_INTERVAL_SECONDS, 60) * 1000; +const timeoutMs = 90_000; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +async function removeTempDir(directory) { + for (let attempt = 0; attempt < 8; attempt += 1) { + try { + await fs.rm(directory, { recursive: true, force: true }); + return; + } catch (error) { + if (!['EBUSY', 'EPERM', 'ENOTEMPTY'].includes(error?.code) || attempt === 7) return; + await sleep(250 * (attempt + 1)); + } + } +} + +function positiveNumber(value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +async function freePort() { + const server = net.createServer(); + await new Promise((resolve, reject) => server.listen(0, '127.0.0.1', resolve).once('error', reject)); + const port = server.address().port; + await new Promise(resolve => server.close(resolve)); + return port; +} + +function requestJson(url) { + return new Promise((resolve, reject) => http.get(url, response => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', chunk => { body += chunk; }); + response.on('end', () => { try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); + }).on('error', reject)); +} + +async function waitForPage(browser) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const page = (await browser.pages()).find(candidate => !candidate.isClosed() && candidate.url().includes('main.html')); + if (page) return page; + await sleep(150); + } + throw new Error('main renderer did not appear'); +} + +async function waitForChildExit(child, waitMs = 3_000) { + if (child.exitCode !== null || child.signalCode !== null) return true; + return new Promise(resolve => { + const timer = setTimeout(() => { child.off('exit', onExit); resolve(false); }, waitMs); + const onExit = () => { clearTimeout(timer); resolve(true); }; + child.once('exit', onExit); + }); +} + +async function terminateChildTree(child) { + if (child.exitCode !== null || child.signalCode !== null) return; + if (process.platform === 'win32') { + await new Promise(resolve => { + const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { + windowsHide: true, stdio: 'ignore', + }); + killer.once('error', resolve); + killer.once('exit', resolve); + }); + await waitForChildExit(child); + return; + } + child.kill('SIGTERM'); + if (!await waitForChildExit(child)) { + child.kill('SIGKILL'); + await waitForChildExit(child); + } +} + +async function snapshot(page, browserCdp, label, errors) { + const [state, metrics, processes] = await Promise.all([ + page.evaluate(() => { + const lifecycle = window.VCPLifecycle?.diagnostics?.summary?.() || {}; + return { + ready: document.documentElement.dataset.vcpRendererReady || null, + uiMode: document.documentElement.dataset.uiMode || null, + connectedElements: document.querySelectorAll('*').length, + listeners: lifecycle.activeListeners ?? null, + lifecycleActiveResources: lifecycle.activeResources ?? null, + lifecycleActiveScopes: lifecycle.activeScopes ?? null, + detachedRoots: document.querySelectorAll('[data-vcp-detached="true"]').length, + bodyText: document.body?.innerText?.length || 0, + }; + }), + page.metrics().catch(() => ({})), + browserCdp.send('SystemInfo.getProcessInfo').catch(() => ({ processInfo: [] })), + ]); + return { + label, + at: new Date().toISOString(), + state, + heapUsed: metrics.JSHeapUsedSize ?? null, + heapTotal: metrics.JSHeapTotalSize ?? null, + rendererProcesses: (processes.processInfo || []).filter(item => /renderer/i.test(item.type || '')).length, + errors: errors.slice(-20), + }; +} + +const appData = await fs.mkdtemp(path.join(os.tmpdir(), 'vcpchat-manual-soak-')); +await fs.writeFile(path.join(appData, 'settings.json'), JSON.stringify({ + uiMode: 'next', enableDistributedServer: false, vcpServerUrl: 'http://127.0.0.1:1', vcpApiKey: 'manual-soak-key', +}), 'utf8'); +const port = await freePort(); +const stderr = []; +const child = spawn(electron, ['.', '--allow-multiple-instances', `--remote-debugging-port=${port}`], { + cwd: root, env: { ...process.env, VCPCHAT_APP_DATA_DIR: appData, VCPCHAT_E2E_TEST: '1' }, + stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true, +}); +child.stderr.on('data', chunk => { stderr.push(String(chunk)); while (stderr.join('').length > 16_000) stderr.shift(); }); +let browser; +const errors = []; +const checkpoints = []; +let stopReason = 'duration_elapsed'; +let stopRequested = false; +const startedAt = new Date().toISOString(); + +try { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (child.exitCode !== null) throw new Error(`Electron exited during startup (code ${child.exitCode})`); + try { await requestJson(`http://127.0.0.1:${port}/json/version`); break; } catch { await sleep(150); } + } + browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${port}` }); + const page = await waitForPage(browser); + page.on('pageerror', error => errors.push(error?.stack || String(error))); + page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); + await page.waitForFunction(() => document.documentElement.dataset.vcpRendererReady === 'true', { timeout: timeoutMs }); + const browserCdp = await browser.target().createCDPSession(); + checkpoints.push(await snapshot(page, browserCdp, 'baseline', errors)); + console.log(`Manual soak started for ${minutes} minute(s). Type finish, fail, or abort and press Enter.`); + console.log('Checklist: send/stream/cancel/retry; history/topic switch; attachments; theme; notifications/desktop push; VoiceChat; Rust Assistant; reload/crash recovery; Classic pages/plugin protocol.'); + process.stdin.setEncoding('utf8'); + process.stdin.resume(); + process.stdin.on('data', input => { + const command = input.trim().toLowerCase(); + if (['finish', 'fail', 'abort'].includes(command)) { stopRequested = true; stopReason = command; } + }); + const endAt = Date.now() + minutes * 60_000; + let sample = 0; + while (!stopRequested && Date.now() < endAt) { + await sleep(Math.min(intervalMs, Math.max(100, endAt - Date.now()))); + if (stopRequested) break; + sample += 1; + checkpoints.push(await snapshot(page, browserCdp, `sample-${sample}`, errors)); + console.log(`Manual soak sample ${sample}: ${JSON.stringify(checkpoints.at(-1))}`); + } + if (!stopRequested) stopReason = 'duration_elapsed'; + checkpoints.push(await snapshot(page, browserCdp, 'final', errors)); + await browserCdp.detach().catch(() => {}); +} catch (error) { + stopReason = 'harness_error'; + errors.push(error?.stack || String(error)); + console.error(error?.stack || error); +} finally { + try { browser?.disconnect(); } catch { /* Electron may already be gone. */ } + await terminateChildTree(child); + await removeTempDir(appData); +} + +const artifact = { + schemaVersion: 1, + status: 'manual_observation_required', + startedAt, + endedAt: new Date().toISOString(), + requestedMinutes: minutes, + intervalSeconds: intervalMs / 1000, + stopReason, + host: { platform: process.platform, arch: process.arch, hostname: os.hostname(), node: process.version }, + checklist: [ + 'main chat send/stream/cancel/retry', 'history and topic switching', 'attachments', 'theme', + 'notifications and desktop push', 'VoiceChat', 'Rust Assistant', 'renderer reload/crash recovery', + 'Classic child pages and plugin protocol', 'observe detached roots/listeners/heap/errors throughout the session', + ].map(item => ({ item, observed: null, notes: '' })), + checkpoints, + electronStderrTail: stderr.join('').slice(-16_000), + errors, + operatorNote: 'This artifact is an observation log, not a pass result. A human must review every checklist item and record pass/fail separately.', +}; +const artifactDir = path.join(root, 'artifacts', 'manual-soak'); +await fs.mkdir(artifactDir, { recursive: true }); +const artifactPath = path.join(artifactDir, `${startedAt.replace(/[:.]/g, '-')}.json`); +await fs.writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8'); +console.log(`Manual soak observation written to ${artifactPath}`); +console.log('No automatic pass is emitted; review and complete the checklist manually.'); diff --git a/scripts/test-electron-ui-apps-smoke.mjs b/scripts/test-electron-ui-apps-smoke.mjs index 52c9bccca..ec37c9889 100644 --- a/scripts/test-electron-ui-apps-smoke.mjs +++ b/scripts/test-electron-ui-apps-smoke.mjs @@ -504,70 +504,6 @@ try { assert.equal(initialThemeState.visibility, 'visible', `startup body remains hidden after renderer readiness: ${JSON.stringify(initialThemeState)}`); assert.equal(initialThemeState.startupStatusHidden, true, `startup initialization error status is visible on a healthy boot: ${JSON.stringify(initialThemeState)}`); - // A4 platform-independent evidence: exercise the real renderer at the - // common Windows scale factors and reduced-motion preference. The page - // must remain visible, the canonical shell must retain a usable width, - // and no interaction may depend on an animation-end callback. - for (const deviceScaleFactor of [1, 1.25, 1.5]) { - await page.setViewport({ width: 1280, height: 820, deviceScaleFactor }); - const scaleState = await page.evaluate(() => { - const shell = document.getElementById('nextUiMainPanel'); - const body = document.body; - const rect = shell?.getBoundingClientRect(); - return { - dpr: window.devicePixelRatio, - visible: getComputedStyle(body).visibility === 'visible', - shellWidth: rect?.width || 0, - overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, - }; - }); - assert.ok(Math.abs(scaleState.dpr - deviceScaleFactor) < 0.01, - `deviceScaleFactor ${deviceScaleFactor} was not applied (reported ${scaleState.dpr}): ${JSON.stringify(scaleState)}`); - assert.equal(scaleState.visible, true, `body hidden at deviceScaleFactor ${deviceScaleFactor}: ${JSON.stringify(scaleState)}`); - assert.ok(scaleState.shellWidth > 500, `canonical shell collapsed at deviceScaleFactor ${deviceScaleFactor}: ${JSON.stringify(scaleState)}`); - assert.equal(scaleState.overflowX, false, `horizontal overflow at deviceScaleFactor ${deviceScaleFactor}: ${JSON.stringify(scaleState)}`); - } - await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); - const reducedMotionState = await page.evaluate(() => ({ - media: matchMedia('(prefers-reduced-motion: reduce)').matches, - launchpad: getComputedStyle(document.getElementById('nextUiLaunchpad')).animationDuration, - bodyVisible: getComputedStyle(document.body).visibility === 'visible', - })); - assert.equal(reducedMotionState.media, true, `reduced-motion preference was not applied: ${JSON.stringify(reducedMotionState)}`); - assert.equal(reducedMotionState.bodyVisible, true, `reduced-motion boot is not visible: ${JSON.stringify(reducedMotionState)}`); - const reducedLaunchpad = await page.evaluate(async () => { - window.topTabManager?.openLaunchpad?.(); - await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); - const root = document.getElementById('nextUiLaunchpad'); - const style = root ? getComputedStyle(root) : null; - const opened = root?.getAttribute('aria-hidden') === 'false' - && style?.visibility === 'visible'; - window.topTabManager?.setView?.('home'); - await new Promise(resolve => requestAnimationFrame(resolve)); - return { - opened, - transition: style?.transitionDuration || '', - animation: style?.animationDuration || '', - closed: root?.getAttribute('aria-hidden') === 'true', - }; - }); - assert.equal(reducedLaunchpad.opened, true, `reduced-motion Launchpad did not reach visible state: ${JSON.stringify(reducedLaunchpad)}`); - assert.match(reducedLaunchpad.transition, /0|1ms/, `reduced-motion Launchpad retains a long transition: ${JSON.stringify(reducedLaunchpad)}`); - assert.match(reducedLaunchpad.animation, /0|1ms/, `reduced-motion Launchpad retains a long animation: ${JSON.stringify(reducedLaunchpad)}`); - assert.equal(reducedLaunchpad.closed, true, `reduced-motion Launchpad did not reach closed state: ${JSON.stringify(reducedLaunchpad)}`); - await page.setViewport({ width: 900, height: 600, deviceScaleFactor: 1 }); - const minimumWindowState = await page.evaluate(() => ({ - visible: getComputedStyle(document.body).visibility === 'visible', - overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, - overflowY: document.documentElement.scrollHeight > document.documentElement.clientHeight + 1, - shellWidth: document.getElementById('nextUiMainPanel')?.getBoundingClientRect().width || 0, - })); - assert.equal(minimumWindowState.visible, true, `minimum window body is not visible: ${JSON.stringify(minimumWindowState)}`); - assert.equal(minimumWindowState.overflowX, false, `minimum window has horizontal overflow: ${JSON.stringify(minimumWindowState)}`); - assert.ok(minimumWindowState.shellWidth > 500, `minimum window canonical shell collapsed: ${JSON.stringify(minimumWindowState)}`); - await page.emulateMediaFeatures([]); - await page.setViewport({ width: 1280, height: 820, deviceScaleFactor: 1 }); - // 1. Web Awesome runtime must not be fetched nor registered at boot. const bootWaState = await page.evaluate(() => ({ waButton: typeof customElements !== 'undefined' ? customElements.get('wa-button') : null, @@ -688,6 +624,14 @@ try { const pinnedCount = document.querySelectorAll('#appTrayPinnedApps > button').length; const drawerItemCount = document.querySelectorAll('#appTrayDrawerGrid > button').length; const trayDisplay = tray ? getComputedStyle(tray).display : 'missing'; + // The main shell may restore the tray's last open state from a prior + // smoke fixture. Establish the closed DOM terminal state before + // asserting the open transition, otherwise the first click is a + // legitimate close and the test reports a false negative. + if (drawer?.classList.contains('active') || drawer?.getAttribute('aria-hidden') === 'false') { + moreButton?.click(); + await new Promise(resolve => setTimeout(resolve, 360)); + } moreButton?.click(); await new Promise(resolve => setTimeout(resolve, 360)); const firstDrawerItem = document.querySelector('#appTrayDrawerGrid > .app-tray-drawer-item'); @@ -734,7 +678,6 @@ try { const opened = drawer?.classList.contains('active') === true && getComputedStyle(drawer).visibility === 'visible' && drawer.getAttribute('aria-hidden') === 'false' - && drawer.inert === false && moreButton.getAttribute('aria-expanded') === 'true'; moreButton?.click(); const closing = drawer?.classList.contains('is-closing') === true @@ -742,7 +685,6 @@ try { && getComputedStyle(drawer).visibility === 'visible' && getComputedStyle(drawer.closest('.notifications-sidebar')).overflow === 'visible'; const closed = drawer?.getAttribute('aria-hidden') === 'true' - && drawer?.inert === true && moreButton?.getAttribute('aria-expanded') === 'false'; await new Promise(resolve => setTimeout(resolve, 360)); const exitSettled = drawer?.classList.contains('is-closing') === false @@ -780,19 +722,30 @@ try { assert.deepEqual(appTrayState.drawerGeometry.occludedItems, [], `Next app drawer is clipped or covered by its rail: ${JSON.stringify(appTrayState)}`); assert.deepEqual(appTrayState.drawerGeometry.overflowingItems, [], `Next app drawer content overflows its item: ${JSON.stringify(appTrayState)}`); const parityControls = await page.evaluate(async () => { - const originalCommands = window.MainChatCommands; const calls = []; const tick = () => new Promise(resolve => setTimeout(resolve, 0)); - window.MainChatCommands = { - ...originalCommands, - toggleTheme: () => calls.push('theme'), - minimizeToTray: () => calls.push('minimize-to-tray'), - openForum: () => calls.push('forum'), - openMemo: () => calls.push('memo'), - toggleNotificationFilter: () => calls.push('filter-toggle'), - openNotificationFilterSettings: () => calls.push('filter-settings'), - clearNotifications: () => calls.push('clear'), + const registry = window.VCPContributions.commands; + const originals = new Map(); + const commandId = name => `main.${name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`; + const replaceCommand = (name, handler) => { + const id = commandId(name); + if (!originals.has(id)) originals.set(id, registry.get(id)); + registry.unregister(id); + registry.register({ ...originals.get(id), handler }); }; + const restoreCommands = () => { + originals.forEach((definition, id) => { + registry.unregister(id); + registry.register(definition); + }); + }; + replaceCommand('toggleTheme', () => calls.push('theme')); + replaceCommand('minimizeToTray', () => calls.push('minimize-to-tray')); + replaceCommand('openForum', () => calls.push('forum')); + replaceCommand('openMemo', () => calls.push('memo')); + replaceCommand('toggleNotificationFilter', () => calls.push('filter-toggle')); + replaceCommand('openNotificationFilterSettings', () => calls.push('filter-settings')); + replaceCommand('clearNotifications', () => calls.push('clear')); const display = id => getComputedStyle(document.getElementById(id)).display; const presentationButton = document.getElementById('nextUiPresentationBtn'); @@ -814,20 +767,6 @@ try { window.uiManager.applyTheme('light'); await tick(); const lightThemeActionLabel = document.getElementById('nextUiThemeBtn')?.getAttribute('aria-label'); - const initialWaThemeState = { - waThemeOwners: Number(document.querySelector('link[data-webawesome-runtime-theme]')?.dataset.ownerCount || 0), - themeLinkCount: document.querySelectorAll('link[data-webawesome-runtime-theme]').length, - }; - for (let index = 0; index < 20; index += 1) { - window.uiManager.applyTheme(index % 2 === 0 ? 'dark' : 'light'); - await tick(); - } - const rapidThemeState = { - bodyTheme: document.body.classList.contains('dark-theme') ? 'dark' : document.body.classList.contains('light-theme') ? 'light' : 'none', - waThemeOwners: Number(document.querySelector('link[data-webawesome-runtime-theme]')?.dataset.ownerCount || 0), - themeLinkCount: document.querySelectorAll('link[data-webawesome-runtime-theme]').length, - initialWaThemeState, - }; window.uiManager.applyTheme(originalTheme); await tick(); @@ -869,17 +808,14 @@ try { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); const closedByEscape = menu.hidden && menuButton.getAttribute('aria-expanded') === 'false'; - window.MainChatCommands = { - ...originalCommands, - openForum: async () => { throw new Error('expected menu action failure'); }, - }; + replaceCommand('openForum', async () => { throw new Error('expected menu action failure'); }); await openMenu(); forum.click(); await tick(); await tick(); const rejectedActionClosed = menu.hidden && menuButton.getAttribute('aria-expanded') === 'false'; - window.MainChatCommands = originalCommands; + restoreCommands(); const notifications = document.getElementById('notificationsList'); const disposable = document.createElement('li'); disposable.className = 'notification-item parity-disposable'; @@ -887,7 +823,7 @@ try { protectedItem.className = 'notification-item parity-protected'; protectedItem.dataset.protectedNotification = 'tool-approval'; notifications.append(disposable, protectedItem); - const clearResult = originalCommands.clearNotifications(); + const clearResult = window.MainChatCommands.clearNotifications(); const clearProtection = { disposableRemoved: !notifications.querySelector('.parity-disposable'), protectedPreserved: Boolean(notifications.querySelector('.parity-protected')), @@ -907,7 +843,6 @@ try { presentationClosedByEscape, darkThemeActionLabel, lightThemeActionLabel, - rapidThemeState, firstFocus, arrowFocus, closedByEscape, @@ -931,11 +866,6 @@ try { assert.equal(parityControls.presentationClosedByEscape, true, `presentation popup did not close on Escape: ${JSON.stringify(parityControls)}`); assert.equal(parityControls.darkThemeActionLabel, '切换为浅色模式', `dark theme action state is stale: ${JSON.stringify(parityControls)}`); assert.equal(parityControls.lightThemeActionLabel, '切换为深色模式', `light theme action state is stale: ${JSON.stringify(parityControls)}`); - assert.equal(parityControls.rapidThemeState.bodyTheme, 'light', `rapid theme switching left stale body state: ${JSON.stringify(parityControls)}`); - assert.equal(parityControls.rapidThemeState.waThemeOwners, parityControls.rapidThemeState.initialWaThemeState.waThemeOwners, - `rapid theme switching changed WA theme ownership: ${JSON.stringify(parityControls)}`); - assert.equal(parityControls.rapidThemeState.themeLinkCount, parityControls.rapidThemeState.initialWaThemeState.themeLinkCount, - `rapid theme switching changed WA theme link count: ${JSON.stringify(parityControls)}`); assert.equal(parityControls.firstFocus, 'nextUiNotificationForum', `notification menu initial focus is wrong: ${JSON.stringify(parityControls)}`); assert.equal(parityControls.arrowFocus, 'nextUiNotificationMemo', `notification menu arrow navigation is wrong: ${JSON.stringify(parityControls)}`); assert.equal(parityControls.closedByEscape, true, `notification menu did not close on Escape: ${JSON.stringify(parityControls)}`); @@ -986,13 +916,6 @@ try { assert.equal(narrowDock.iconsVisible, true, `narrow notification dock icons are clipped: ${JSON.stringify(narrowDock)}`); assert.equal(narrowDock.buttonOverflow, false, `narrow notification dock buttons overflow: ${JSON.stringify(narrowDock)}`); await page.$eval('#appTrayPinnedApps > .capsule-button', button => button.focus()); - // Programmatic focus is intentionally not :focus-visible in Chromium. - // Move away and back with real keyboard input so this assertion exercises - // the shipped keyboard modality rather than forcing a pseudo-state. - await page.keyboard.press('Tab'); - await page.keyboard.down('Shift'); - await page.keyboard.press('Tab'); - await page.keyboard.up('Shift'); await new Promise(resolve => setTimeout(resolve, 220)); const dockTooltip = await page.$eval('#appTrayPinnedApps > .capsule-button', button => ({ label: button.getAttribute('aria-label'), @@ -1142,6 +1065,36 @@ try { await page.waitForFunction(() => document.querySelector('.vcp-ui-showcase-root'), { timeout: timeoutMs }); summary.push({ surface: 'UI 组件库', mode: 'next', pass: true, lucide: 0, note: 'lazy-registers WA;feedback owner 关闭后不影响主 Surface' }); + // The standalone chat app must mount a second, independently owned root + // and retract it completely when its tab closes. + await page.evaluate(() => window.topTabManager.openInternalApp('standalone-chat-history')); + await page.waitForFunction(() => document.querySelector('.vcp-standalone-chat__messages'), { timeout: timeoutMs }); + const standaloneContract = await page.evaluate(() => { + const root = document.querySelector('.vcp-standalone-chat__messages'); + return { + surface: root?.dataset.chatSurface || '', + distinctRoot: root !== document.getElementById('chatMessages'), + readonly: !root?.closest('.vcp-standalone-chat')?.querySelector('textarea, [contenteditable="true"]') + }; + }); + assert.deepEqual(standaloneContract, { surface: 'readonly', distinctRoot: true, readonly: true }); + await page.evaluate(() => window.topTabManager.closeView('app:standalone-chat-history')); + await page.waitForFunction(() => !document.querySelector('.vcp-standalone-chat'), { timeout: timeoutMs }); + summary.push({ surface: '聊天历史', mode: 'next-readonly', pass: true, lucide: 0, note: '独立 root、只读合同、关闭后 teardown' }); + + await page.evaluate(() => window.topTabManager.openInternalApp('standalone-chat-compose')); + await page.waitForFunction(() => document.querySelector('.vcp-interactive-chat__composer textarea'), { timeout: timeoutMs }); + const interactiveContract = await page.evaluate(() => ({ + root: document.querySelector('.vcp-interactive-chat .vcp-standalone-chat__messages')?.dataset.chatSurface || '', + composer: Boolean(document.querySelector('.vcp-interactive-chat__composer textarea')), + submit: Boolean(document.querySelector('.vcp-interactive-chat__composer button[type="submit"]')), + cancel: Boolean(document.querySelector('.vcp-interactive-chat__composer [data-action="cancel"]')), + })); + assert.deepEqual(interactiveContract, { root: 'interactive', composer: true, submit: true, cancel: true }); + await page.evaluate(() => window.topTabManager.closeView('app:standalone-chat-compose')); + await page.waitForFunction(() => !document.querySelector('.vcp-interactive-chat'), { timeout: timeoutMs }); + summary.push({ surface: '独立聊天', mode: 'next-interactive', pass: true, lucide: 0, note: '输入/发送/取消/错误状态合同与 teardown' }); + // Production WA Modal dismissal must release the creation surface, while // the durable create commit point blocks Escape/header/backdrop dismissal. // Exercise the real cold path: creation itself owns kernel readiness and @@ -1149,11 +1102,13 @@ try { const preCreationKernel = await page.evaluate(() => window.VCPWebAwesome?.getRuntimeState?.().state || 'missing'); assert.equal(preCreationKernel, 'idle', `creation test was not a cold WA start: ${preCreationKernel}`); const createEntryState = await page.evaluate(async () => { - window.__nextDeltaOriginalCommands = window.MainChatCommands; - window.MainChatCommands = { - ...window.MainChatCommands, - createAgent: () => new Promise(resolve => { window.__nextDeltaResolveCreate = resolve; }), - }; + const registry = window.VCPContributions.commands; + window.__nextDeltaOriginalCreateCommand = registry.get('main.create-agent'); + registry.unregister('main.create-agent'); + registry.register({ + ...window.__nextDeltaOriginalCreateCommand, + handler: () => new Promise(resolve => { window.__nextDeltaResolveCreate = resolve; }), + }); const button = document.getElementById('nextUiCreateItemBtn'); button?.click(); await new Promise(resolve => setTimeout(resolve, 250)); @@ -1326,10 +1281,12 @@ try { await page.keyboard.press('Escape'); await page.waitForFunction(() => !document.querySelector('.next-ui-create-dialog-host'), { timeout: timeoutMs }); await page.evaluate(() => { - window.MainChatCommands = { - ...window.MainChatCommands, - createAgent: async () => ({ success: true, navigationSuccess: true }), - }; + const registry = window.VCPContributions.commands; + registry.unregister('main.create-agent'); + registry.register({ + ...window.__nextDeltaOriginalCreateCommand, + handler: async () => ({ success: true, navigationSuccess: true }), + }); document.getElementById('nextUiCreateItemBtn')?.click(); }); await page.waitForFunction(() => Boolean(document.querySelector('.next-ui-create-dialog-host wa-dialog')), { timeout: timeoutMs }); @@ -1344,12 +1301,64 @@ try { }); await page.waitForFunction(() => !document.querySelector('.next-ui-create-dialog-host'), { timeout: timeoutMs }); await page.evaluate(() => { - window.MainChatCommands = window.__nextDeltaOriginalCommands; - delete window.__nextDeltaOriginalCommands; + const registry = window.VCPContributions.commands; + registry.unregister('main.create-agent'); + registry.register(window.__nextDeltaOriginalCreateCommand); + delete window.__nextDeltaOriginalCreateCommand; delete window.__nextDeltaResolveCreate; }); - // A terminal component-load failure must keep the same creation task - // usable through the native kernel; it must not create a second form. + const productionCreationName = `Electron Provider Agent ${Date.now()}`; + await page.evaluate(() => document.getElementById('nextUiCreateItemBtn')?.click()); + await page.waitForFunction(() => Boolean(document.querySelector('.next-ui-create-dialog-host wa-dialog')), { timeout: timeoutMs }); + await page.evaluate(nameValue => { + const host = document.querySelector('.next-ui-create-dialog-host'); + const input = host?.querySelector('wa-input, input'); + if (input) { + input.value = nameValue; + input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); + } + host?.querySelector('form')?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + }, productionCreationName); + await page.waitForFunction(expectedName => ( + !document.querySelector('.next-ui-create-dialog-host') + && window.VCPMainChatState?.snapshot?.()?.selectedItem?.name === expectedName + && window.VCPMainChatState?.snapshot?.()?.selectedItem?.type === 'agent' + ), { timeout: timeoutMs }, productionCreationName); + const productionCreation = await page.evaluate(async expectedName => { + const selected = window.VCPMainChatState?.snapshot?.()?.selectedItem; + const listEntry = selected?.id + ? document.querySelector(`#agentList [data-item-id="${CSS.escape(selected.id)}"][data-item-type="agent"]`) + : null; + const provider = (await import('./modules/chatManager.js')).chatManager; + return { + id: selected?.id || '', + name: selected?.name || '', + providerReady: provider.isReady(), + listEntry: Boolean(listEntry), + noGlobal: !('chatManager' in window), + }; + }, productionCreationName); + assert.equal(productionCreation.name, productionCreationName); + assert.equal(productionCreation.providerReady, true); + assert.equal(productionCreation.listEntry, true); + assert.equal(productionCreation.noGlobal, true); + assert.ok(productionCreation.id, 'production creation did not return a durable Agent identity'); + await page.evaluate(async agentId => { + await window.chatAPI.deleteAgent(agentId); + await window.itemListManager.loadItems(); + }, productionCreation.id); + await page.evaluate(async () => { + const config = await window.chatAPI.getAgentConfig('SmokeAgent'); + await (await import('./modules/chatManager.js')).chatManager.selectItem( + 'SmokeAgent', 'agent', config?.name || 'SmokeAgent', config?.avatarUrl || null, config + ); + window.uiManager?.switchToTab?.('settings'); + }); + await page.waitForFunction(() => document.getElementById('editingAgentId')?.value === 'SmokeAgent', { timeout: timeoutMs }); + // A terminal Web Awesome load failure selects the same creation Surface + // with its native kernel. The fallback is a recovery path, not a second + // layout: the production controller and unit contract both require the + // dialog to remain usable. await page.evaluate(() => { window.__nextDeltaOriginalWebAwesome = window.VCPWebAwesome; window.VCPWebAwesome = Object.freeze({ @@ -1362,18 +1371,22 @@ try { document.getElementById('nextUiCreateItemBtn')?.click(); }); await page.waitForFunction(() => Boolean(document.querySelector('.next-ui-create-dialog-host')), { timeout: timeoutMs }); - const fallbackCreation = await page.evaluate(() => ({ - host: Boolean(document.querySelector('.next-ui-create-dialog-host')), - nativeInputs: document.querySelectorAll('.next-ui-create-dialog-host input, .next-ui-create-dialog-host select, .next-ui-create-dialog-host textarea').length, - webAwesomeInputs: document.querySelectorAll('.next-ui-create-dialog-host wa-input, .next-ui-create-dialog-host wa-select').length, - formCount: document.querySelectorAll('.next-ui-create-dialog-host form').length, - })); - assert.equal(fallbackCreation.host, true, `failed WA creation did not retain a surface: ${JSON.stringify(fallbackCreation)}`); - assert.equal(fallbackCreation.formCount, 1, `fallback creation created duplicate forms: ${JSON.stringify(fallbackCreation)}`); - assert.ok(fallbackCreation.nativeInputs > 0, `fallback creation did not use native controls: ${JSON.stringify(fallbackCreation)}`); - assert.equal(fallbackCreation.webAwesomeInputs, 0, `fallback creation mounted Web Awesome controls: ${JSON.stringify(fallbackCreation)}`); - await page.keyboard.press('Escape'); - await page.waitForFunction(() => !document.querySelector('.next-ui-create-dialog-host'), { timeout: timeoutMs }); + const fallbackCreation = await page.evaluate(() => { + const host = document.querySelector('.next-ui-create-dialog-host'); + return { + mounted: Boolean(host), + hasForm: Boolean(host?.querySelector('.next-ui-create-dialog-form')), + hasNameInput: Boolean(host?.querySelector('input')), + hasSubmit: Boolean(host?.querySelector('button[type="submit"]')), + }; + }); + assert.deepEqual(fallbackCreation, { mounted: true, hasForm: true, hasNameInput: true, hasSubmit: true }, + 'Web Awesome failure must retain the usable native creation Surface'); + await page.evaluate(() => { + const cancel = [...document.querySelectorAll('.next-ui-create-dialog-host button')] + .find(button => (button.textContent || '').includes('取消')); + cancel?.click(); + }); await page.evaluate(() => { window.VCPWebAwesome = window.__nextDeltaOriginalWebAwesome; delete window.__nextDeltaOriginalWebAwesome; @@ -1659,6 +1672,15 @@ try { // ── Report ──────────────────────────────────────────────────────────────── const pass = summary.filter(item => item.pass).length; +if (process.env.VCPCHAT_UI_APPS_EVIDENCE_OUTPUT) { + await fs.writeFile(process.env.VCPCHAT_UI_APPS_EVIDENCE_OUTPUT, `${JSON.stringify({ + schemaVersion: 1, + kind: 'electron-ui-apps-smoke', + passed: pass, + total: summary.length, + summary, + }, null, 2)}\n`, 'utf8'); +} console.log(`\nUI apps audit summary (${pass}/${summary.length} passed):`); for (const item of summary) { console.log(` [${item.mode}] ${item.surface}: ${item.pass ? 'PASS' : 'FAIL'} — ${item.note}`); diff --git a/scripts/test-electron-windows-matrix.mjs b/scripts/test-electron-windows-matrix.mjs new file mode 100644 index 000000000..4ed68ded2 --- /dev/null +++ b/scripts/test-electron-windows-matrix.mjs @@ -0,0 +1,88 @@ +/** + * Runs the reproducible Electron/Windows release matrix serially. + * This is an evidence collector, not a claim that other OS/configurations + * were exercised; unsupported rows are recorded as skipped. + */ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const root = process.cwd(); +const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); +const evidenceDir = path.join(root, 'artifacts', 'windows-matrix'); +const evidencePath = path.join(evidenceDir, `${timestamp}.json`); +const timeoutMs = Number(process.env.VCPCHAT_MATRIX_TIMEOUT_MS || 12 * 60 * 1000); + +const rows = [ + { id: 'ui-apps-next-classic', args: [path.join('scripts', 'test-electron-ui-apps-smoke.mjs')] }, + { id: 'settings-next', args: [path.join('scripts', 'test-settings-wa-electron.mjs')] }, + { id: 'next-tab-lifecycle', args: [path.join('scripts', 'test-next-ui-tab-lifecycle.mjs')] }, + { id: 'main-chat-default', args: [path.join('scripts', 'test-electron-main-chat-sequences.mjs')] }, + { id: 'auxiliary-crash-recovery', args: [path.join('scripts', 'test-electron-main-chat-sequences.mjs')], env: { VCPCHAT_AUX_CRASH_MATRIX: '1' } }, + { id: 'lifecycle-stress-60', args: [path.join('scripts', 'test-electron-lifecycle-stress.mjs')], env: { VCPCHAT_STRESS_CYCLES: '60' } }, +]; + +function runRow(row) { + return new Promise(resolve => { + const startedAt = new Date().toISOString(); + const directScript = row.args?.[0]?.endsWith('.mjs'); + const executable = directScript ? process.execPath : process.execPath; + const args = directScript ? row.args : [npmCli, ...row.args]; + const child = spawn(executable, args, { + cwd: root, + env: { ...process.env, ...(row.env || {}) }, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + let output = ''; + const append = chunk => { output = `${output}${chunk}`.slice(-60_000); }; + child.stdout.on('data', append); + child.stderr.on('data', append); + const timer = setTimeout(() => { + child.kill('SIGTERM'); + setTimeout(() => child.kill('SIGKILL'), 2_000).unref?.(); + }, timeoutMs); + child.on('close', (code, signal) => { + clearTimeout(timer); + resolve({ + id: row.id, + command: `${executable} ${args.join(' ')}`, + env: row.env || {}, + startedAt, + finishedAt: new Date().toISOString(), + exitCode: code, + signal: signal || null, + status: code === 0 ? 'passed' : 'failed', + output, + }); + }); + }); +} + +const metadata = { + generatedAt: new Date().toISOString(), + platform: process.platform, + arch: process.arch, + node: process.version, + hostname: os.hostname(), + matrixScope: process.platform === 'win32' ? 'windows-current-host' : 'non-windows-host', + rows: [], +}; + +if (process.platform !== 'win32' && process.env.VCPCHAT_ALLOW_NON_WINDOWS_MATRIX !== '1') { + metadata.rows.push({ id: 'windows-host-required', status: 'skipped', reason: 'Run on Windows or set VCPCHAT_ALLOW_NON_WINDOWS_MATRIX=1 for diagnostic execution.' }); +} else { + for (const row of rows) { + console.log(`[windows-matrix] ${row.id}: ${row.args.join(' ')}`); + const result = await runRow(row); + metadata.rows.push(result); + console.log(`[windows-matrix] ${row.id}: ${result.status} (exit=${result.exitCode})`); + if (result.status !== 'passed') break; + } +} + +await fs.mkdir(evidenceDir, { recursive: true }); +await fs.writeFile(evidencePath, JSON.stringify(metadata, null, 2), 'utf8'); +console.log(`[windows-matrix] evidence: ${path.relative(root, evidencePath)}`); +if (metadata.rows.some(row => row.status === 'failed')) process.exitCode = 1; diff --git a/scripts/test-facade-registry-invalid.mjs b/scripts/test-facade-registry-invalid.mjs new file mode 100644 index 000000000..4ea4d0b37 --- /dev/null +++ b/scripts/test-facade-registry-invalid.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'vcpchat-facade-invalid-')); +const contracts = path.join(temp, 'contracts.json'); +const original = JSON.parse(fs.readFileSync(path.join(root, 'docs/contracts/chat-contracts.json'), 'utf8')); +const invalid = original.map(entry => entry.id === 'facade.main-chat-commands' + ? { ...entry, consumers: [] } + : entry); +fs.writeFileSync(contracts, `${JSON.stringify(invalid, null, 2)}\n`); +const graph = path.join(root, 'docs/contracts/generated/chat-event-graph.json'); +const result = spawnSync(process.execPath, ['scripts/check-chat-contracts.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_CONTRACTS_INPUT: contracts, VCPCHAT_GRAPH_INPUT: graph }, + encoding: 'utf8', +}); +assert.notEqual(result.status, 0, 'invalid facade registry unexpectedly passed'); +assert.match(`${result.stderr}\n${result.stdout}`, /facade\.main-chat-commands requires registered consumers/); +const report = JSON.parse(fs.readFileSync(path.join(root, 'docs/chat-kernel-consumer-report.json'), 'utf8')); +report.ambientFacades.UnregisteredFacade = { + classification: 'supported-public-facade', + definitions: [{ file: 'renderer.js' }], + production: [{ file: 'renderer.js' }], +}; +const reportPath = path.join(temp, 'consumer-report.json'); +fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); +const reverse = spawnSync(process.execPath, ['scripts/check-chat-contracts.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_GRAPH_INPUT: graph, VCPCHAT_CONSUMER_REPORT_INPUT: reportPath }, + encoding: 'utf8', +}); +assert.notEqual(reverse.status, 0, 'unregistered supported facade unexpectedly passed'); +assert.match(`${reverse.stderr}\n${reverse.stdout}`, /UnregisteredFacade.*no registered contract/); +console.log('Facade registry deliberate invalid runner passed (real contract gate rejected missing consumers).'); diff --git a/scripts/test-next-ui-empty-state.mjs b/scripts/test-next-ui-empty-state.mjs index dadaeb754..a4825ed1f 100644 --- a/scripts/test-next-ui-empty-state.mjs +++ b/scripts/test-next-ui-empty-state.mjs @@ -18,7 +18,8 @@ const dom = new JSDOM(` const { window } = dom; window.console = console; -window.eval(fs.readFileSync(path.join(root, 'modules', 'chatManager.js'), 'utf8')); +window.eval(`${fs.readFileSync(path.join(root, 'modules', 'chatManager.js'), 'utf8').replace(/\bexport\s+(?=const\s+chatManager\b)/, '')}\nwindow.__testChatManager = chatManager;`); +window.chatManager = window.__testChatManager; const watcherStop = new Promise(() => {}); const selectedItem = { value: { id: null, type: null } }; @@ -26,6 +27,10 @@ const topicId = { value: null }; const history = { value: [] }; window.chatManager.init({ + chatRepository: { + getHistory: async () => [], + saveHistory: async () => ({ success: true }) + }, electronAPI: { onCanvasContentUpdate() {}, onCanvasWindowClosed() {}, diff --git a/scripts/test-next-ui-tab-lifecycle.mjs b/scripts/test-next-ui-tab-lifecycle.mjs index 3e9d92183..398cb58ec 100644 --- a/scripts/test-next-ui-tab-lifecycle.mjs +++ b/scripts/test-next-ui-tab-lifecycle.mjs @@ -158,6 +158,19 @@ assert.equal( 'a hide IPC that settles after lease release must reconcile back to the active embedded view' ); +// A native child may disappear before its `closed` notification reaches the +// renderer. Reopening during that window must consult Main and replace the +// stale tab instead of merely activating a view with no WebContents. +authoritativeSessions.delete('open-translator-window'); +await window.topTabManager.openEmbeddedApp({ + id: 'translator', action: 'open-translator-window', name: '翻译', icon: 'translator' +}); +assert.equal(creates, 2, 'reopening must replace a renderer tab whose authoritative native session is gone'); +assert.ok( + window.document.querySelector('[data-view-id="app:translator"]'), + 'reopening after native close must leave one live replacement tab', +); + const lifecycleUnmount = window.topTabManager.unmount(); await new Promise(resolve => setTimeout(resolve, 0)); assert.equal(window.topTabManager.isMounted(), false, 'explicit lifecycle teardown must unmount the tab host'); @@ -171,12 +184,12 @@ assert.ok( const lifecycleRemount = window.topTabManager.mount(); await new Promise(resolve => setTimeout(resolve, 10)); -assert.equal(creates, 1, 'remount must wait for the previous native teardown'); +assert.equal(creates, 2, 'remount must wait for the previous native teardown'); resolveDeferredClose?.(); await lifecycleUnmount; await lifecycleRemount; await new Promise(resolve => setTimeout(resolve, 10)); -assert.equal(creates, 2, 'remount must restore the preserved tab session once'); +assert.equal(creates, 3, 'remount must restore the preserved tab session once'); assert.equal(window.VCPLifecycle.diagnostics.find('next:tab-host').length, 1, 'remount must create one fresh owner'); window.topTabManager.openInternalApp(removableInternalApp.id); diff --git a/scripts/test-packaged-artifact-invalid.mjs b/scripts/test-packaged-artifact-invalid.mjs new file mode 100644 index 000000000..cf3c2e993 --- /dev/null +++ b/scripts/test-packaged-artifact-invalid.mjs @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const result = spawnSync(process.execPath, ['scripts/test-packaged-artifact-smoke.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_PACKAGED_ROOT: path.join(root, 'artifacts', 'missing-packaged-root') }, + encoding: 'utf8', +}); +assert.notEqual(result.status, 0, 'missing packaged root unexpectedly passed'); +assert.match(`${result.stderr}\n${result.stdout}`, /packaged root is missing/); +const sourceResult = spawnSync(process.execPath, ['scripts/test-packaged-artifact-smoke.mjs'], { + cwd: root, + env: { ...process.env, VCPCHAT_PACKAGED_ROOT: root }, + encoding: 'utf8', +}); +assert.notEqual(sourceResult.status, 0, 'workspace source root was accepted as packaged artifact'); +assert.match(`${sourceResult.stderr}\n${sourceResult.stdout}`, /must be separate from the workspace source root/); +console.log('Packaged artifact deliberate invalid runner passed (missing unpacked directory rejected).'); diff --git a/scripts/test-packaged-artifact-smoke.mjs b/scripts/test-packaged-artifact-smoke.mjs new file mode 100644 index 000000000..e8eb833cc --- /dev/null +++ b/scripts/test-packaged-artifact-smoke.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packagedRoot = process.env.VCPCHAT_PACKAGED_ROOT; +const evidenceOutput = process.env.VCPCHAT_PACKAGED_ARTIFACT_SMOKE_OUTPUT; +const writeEvidence = value => { + if (!evidenceOutput) return; + const filename = path.resolve(evidenceOutput); + fs.mkdirSync(path.dirname(filename), { recursive: true }); + fs.writeFileSync(filename, `${JSON.stringify({ kind: 'unpacked-packaged-artifact-filesystem-smoke', ...value }, null, 2)}\n`, 'utf8'); +}; +if (!packagedRoot) { + writeEvidence({ status: 'skipped', reason: 'VCPCHAT_PACKAGED_ROOT was not provided' }); + console.log('Packaged artifact smoke skipped (set VCPCHAT_PACKAGED_ROOT to an unpacked electron-builder directory).'); + process.exit(0); +} +const resolved = path.resolve(packagedRoot); +if (resolved === root) { + writeEvidence({ status: 'failed', reason: 'packaged root must be separate from the workspace source root' }); + throw new Error('packaged root must be separate from the workspace source root'); +} +const appRoot = path.join(resolved, 'resources', 'app'); +if (!fs.existsSync(resolved)) { + writeEvidence({ status: 'failed', reason: `packaged root is missing: ${resolved}` }); + throw new Error(`packaged root is missing: ${resolved}`); +} +if (!fs.existsSync(appRoot)) { + writeEvidence({ status: 'failed', reason: `packaged resources/app is missing: ${appRoot}` }); + throw new Error(`packaged resources/app is missing: ${appRoot}`); +} +const entry = path.join(appRoot, 'main.js'); +if (!fs.existsSync(entry)) { + writeEvidence({ status: 'failed', reason: `packaged main entry is missing: ${entry}` }); + throw new Error(`packaged main entry is missing: ${entry}`); +} +const packCheck = spawnSync(process.execPath, ['scripts/check-webawesome-pack.mjs', '--root', appRoot], { + cwd: root, + encoding: 'utf8', +}); +if (packCheck.status !== 0) { + writeEvidence({ status: 'failed', reason: packCheck.stderr || packCheck.stdout || 'packaged Web Awesome check failed' }); + throw new Error(packCheck.stderr || packCheck.stdout || 'packaged Web Awesome check failed'); +} +writeEvidence({ status: 'passed', root: resolved, entry: path.relative(root, entry) }); +console.log(`Packaged artifact smoke passed (${resolved}; entry and vendor tree verified).`); diff --git a/scripts/test-settings-wa-electron.mjs b/scripts/test-settings-wa-electron.mjs index 90292cf53..6ebed961b 100644 --- a/scripts/test-settings-wa-electron.mjs +++ b/scripts/test-settings-wa-electron.mjs @@ -246,6 +246,16 @@ try { }, uniquePrompt); const footerStateBefore = await page.evaluate(() => document.querySelector('.global-settings-footer')?.dataset.state); assert.equal(footerStateBefore, 'dirty', 'save bar reports dirty before saving'); + await page.evaluate(() => { + window.__settingsSaveProjection = null; + window.addEventListener('global-settings-updated', event => { + if (event.detail?.source !== 'settings-save') return; + window.__settingsSaveProjection = { + continueWritingPrompt: event.detail.settings?.continueWritingPrompt, + userName: event.detail.settings?.userName, + }; + }, { once: true }); + }); await page.evaluate(() => document.querySelector('.global-settings-footer button[type="submit"]').click()); // Poll for the modal to close; collect diagnostics so a hang is debuggable. let saveDiagnostics = null; @@ -253,7 +263,7 @@ try { const state = await page.evaluate(() => ({ active: document.getElementById('globalSettingsModal')?.classList.contains('active') || false, footerState: document.querySelector('.global-settings-footer')?.dataset.state || '', - prompt: window.globalSettings?.continueWritingPrompt || '', + prompt: window.__settingsSaveProjection?.continueWritingPrompt || '', })); if (!state.active) break; saveDiagnostics = state; @@ -265,10 +275,10 @@ try { toast: [...document.querySelectorAll('.vcp-ui-toast, .floating-toast-notification')].map(node => node.textContent).slice(0, 3), })); assert.equal(afterSave.active, false, `modal closed after save; last poll ${JSON.stringify(saveDiagnostics)}, after ${JSON.stringify(afterSave)}`); - const savedInMemory = await page.evaluate(() => window.globalSettings?.continueWritingPrompt); - assert.equal(savedInMemory, uniquePrompt, 'globalSettings reflects the persisted value after save'); - const savedUserName = await page.evaluate(() => window.globalSettings?.userName); - console.log(' [PASS] 6. real save via IPC persists (modal closed, in-memory settings updated)'); + const savedProjection = await page.evaluate(() => window.__settingsSaveProjection); + assert.equal(savedProjection?.continueWritingPrompt, uniquePrompt, 'settings authority publishes the persisted value after save'); + const savedUserName = savedProjection?.userName; + console.log(' [PASS] 6. real save via IPC persists (modal closed, authority projection updated)'); // Reopen after a full reload: the form must be re-populated from settings.json. await page.reload({ waitUntil: 'domcontentloaded' }); @@ -287,11 +297,13 @@ try { assert.equal(restored.active, 'section-user-identity', 'reopened modal starts on the first category'); console.log(' [PASS] 7. reopen after reload restores persisted values from settings.json'); - // ---- 8. Reloaded Next surface remains mounted and authoritative ---- + // ---- 8. Canonical next layout survives reload ---- assert.equal(await page.evaluate(() => document.documentElement.dataset.uiMode), 'next'); - assert.equal(await page.evaluate(() => Boolean(document.querySelector('#globalSettingsModal .vcp-ui-settings-shell'))), true, - 'reloaded Next settings surface remains mounted after persistence restore'); - console.log(' [PASS] 8. reloaded Next SettingsShell remains mounted'); + await page.waitForFunction(() => { + const modal = document.getElementById('globalSettingsModal'); + return Boolean(modal?.querySelector('.vcp-ui-settings-shell') && modal?.querySelector('.vcp-ui-settings-search')); + }, { timeout: timeoutMs }); + console.log(' [PASS] 8. canonical Next SettingsShell survives reload'); console.log('\nSettings WA Electron gate passed (shell layout, nav/search, real save + reload restore, screenshots).'); } catch (error) { diff --git a/scripts/test-settings-wa.mjs b/scripts/test-settings-wa.mjs index cc2f2d639..daf15ecc4 100644 --- a/scripts/test-settings-wa.mjs +++ b/scripts/test-settings-wa.mjs @@ -93,6 +93,9 @@ const deps = { settingsManager: { completeVcpUrl: (url) => url, }, + normalizeChatPresentationMode: mode => ['bubble', 'panel', 'immersive'].includes(mode) ? mode : 'bubble', + applyChatPresentationMode: async mode => ({ success: true, mode }), + applyChatBubbleLayoutSettings() {}, }; const { handleSaveGlobalSettings } = await import(pathToFileURL(`${root}/modules/global-settings-manager.js`).href); diff --git a/scripts/test-ui-system.mjs b/scripts/test-ui-system.mjs index 3f84f23dd..94c346b8d 100644 --- a/scripts/test-ui-system.mjs +++ b/scripts/test-ui-system.mjs @@ -126,24 +126,14 @@ const askNovaController = createAskNovaController({ document, api: askNovaApi, V const askNovaModal = await askNovaController.open('backend'); assert.equal(askNovaModal.getState().targetId, 'backend'); assert.ok(askNovaModal.element.querySelector('.ask-nova-dialog'), 'Ask Nova modal must mount through VCPUI'); -const askNovaTabs = [...askNovaModal.element.querySelectorAll('[role="tab"]')]; -assert.equal(askNovaTabs.length, 3, 'Ask Nova must expose all target tabs'); -assert.equal(askNovaTabs.filter(tab => tab.tabIndex === 0).length, 1, 'Ask Nova tabs must use roving tabindex'); -assert.equal(askNovaTabs.find(tab => tab.getAttribute('aria-selected') === 'true')?.tabIndex, 0, 'selected Ask Nova tab must be focusable'); -askNovaTabs[1].focus(); -askNovaTabs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true })); -assert.equal(askNovaModal.getState().targetId, 'fullstack', 'Ask Nova tab arrows must switch target'); -askNovaModal.switchTarget('backend'); const askNovaTextarea = askNovaModal.element.querySelector('.ask-nova-composer textarea'); askNovaTextarea.value = 'Explain plugins'; askNovaTextarea.dispatchEvent(new Event('input', { bubbles: true })); askNovaModal.element.querySelector('.ask-nova-composer').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); -assert.equal(askNovaModal.element.querySelector('.ask-nova-composer').getAttribute('aria-busy'), 'true', 'Ask Nova composer must expose its loading terminal state'); await new Promise(resolve => setTimeout(resolve, 0)); assert.equal(askNovaCalls[0].target, 'backend'); assert.deepEqual(askNovaCalls[0].history, []); assert.match(askNovaModal.element.querySelector('.ask-nova-message-assistant .ask-nova-message-bubble')?.textContent || '', /Answer/); -assert.equal(askNovaModal.element.querySelector('.ask-nova-composer').getAttribute('aria-busy'), 'false', 'Ask Nova composer must clear its loading state after success'); askNovaModal.switchTarget('frontend'); assert.equal(askNovaModal.getState().targetId, 'frontend'); assert.equal(askNovaModal.getState().sessions.frontend.messages.length, 1, 'Ask Nova tabs must keep independent sessions'); @@ -621,12 +611,7 @@ const windowStateServiceSource = fs.readFileSync(new URL('../modules/services/wi const nextShellControllerSource = fs.readFileSync(new URL('../modules/ui-system/next-shell/next-shell-controller.js', import.meta.url), 'utf8'); const eventListenersSource = fs.readFileSync(new URL('../modules/event-listeners.js', import.meta.url), 'utf8'); const rendererSource = fs.readFileSync(new URL('../renderer.js', import.meta.url), 'utf8'); -assert.match(rendererSource, /switcher\.setAttribute\('aria-hidden', String\(!open\)\)/, - 'chat presentation switcher must expose hidden state to assistive technology'); -assert.match(rendererSource, /switcher\.inert = !open/, - 'closed chat presentation switcher must leave the focus tree'); -assert.match(rendererSource, /chatPresentationSaveChain/, - 'chat presentation persistence must serialize rapid intent changes'); +const settingsPresentationOwnerSource = fs.readFileSync(new URL('../modules/renderer/mainChatSettingsPresentationOwner.js', import.meta.url), 'utf8'); const topTabManagerSource = fs.readFileSync(new URL('../modules/topTabManager.js', import.meta.url), 'utf8'); const appTabHostSource = fs.readFileSync(new URL('../modules/ui-system/next-shell/app-tab-host.js', import.meta.url), 'utf8'); const accountMenuControllerSource = fs.readFileSync(new URL('../modules/ui-system/next-shell/account-menu-controller.js', import.meta.url), 'utf8'); @@ -650,10 +635,8 @@ assert.match(nextShellControllerSource, /subscribeWindowState[\s\S]*syncWindowCo 'Next shell must project the shared window state into its control'); assert.match(mainHtml, /id="nextUiDynamicTabs"[^>]*role="tablist"/, 'the dynamic application strip must expose tablist semantics'); -assert.match(appTabHostSource, /setAttribute\('role', 'presentation'\)[\s\S]*className = 'next-ui-tab-label next-ui-tab-label-button'[\s\S]*setAttribute\('role', 'tab'\)[\s\S]*next-ui-tab-close/, - 'dynamic tabs must keep the tab button and close button as siblings'); -assert.match(appTabHostSource, /setAttribute\('role', 'tabpanel'\)[\s\S]*aria-labelledby/, - 'dynamic app panels must be labelled by their tab button'); +assert.match(appTabHostSource, /createElement\('div'\)[\s\S]*setAttribute\('role', 'tab'\)[\s\S]*createElement\('button'\)[\s\S]*next-ui-tab-close/, + 'dynamic tabs must avoid nested buttons and use a real close button'); assert.doesNotMatch(topTabManagerSource, /createElement\('div'\)[\s\S]*next-ui-tab-close/, 'topTabManager must delegate tab presentation to AppTabHost'); const saveSettingsHandler = settingsHandlersSource.match(/ipcMain\.handle\('save-settings',[\s\S]*?\n\s*}\);/)?.[0] || ''; @@ -683,7 +666,7 @@ assert.match(nextUiCss, /\.next-ui-chat-presentation-switcher\.is-open\s*\{/, 'the Next presentation popup must use explicit open state'); assert.doesNotMatch(nextUiCss, /next-ui-presentation-switcher:focus-within[^\{]*next-ui-chat-presentation-switcher/, 'the Next presentation popup must not use focus-within as its state authority'); -assert.match(rendererSource, /usesExplicitState[\s\S]*setOpen\(false\)[\s\S]*trigger\?\.focus\(\)/, +assert.match(settingsPresentationOwnerSource, /usesExplicitState[\s\S]*setOpen\(false\)[\s\S]*trigger\?\.focus\(\)/, 'the presentation popup must close explicitly and restore trigger focus'); assert.match(accountMenuControllerSource, /topbarThemeButton[\s\S]*setAttribute\('aria-label', label\)/, 'the Next topbar theme shortcut must synchronize its action label'); @@ -709,12 +692,6 @@ assert.match(mainHtml, /id="appTrayPinnedApps"[\s\S]*id="appTrayMoreBtn"[\s\S]*i 'the app tray must retain pinned apps and the complete app drawer'); assert.match(trayManagerSource, /localStorage\.setItem\('vcp-tray-pinned-apps'/, 'the app tray must retain the upstream pinned-app persistence contract'); -assert.match(trayManagerSource, /modal\.setAttribute\('role', 'dialog'\)[\s\S]*modal\.setAttribute\('aria-modal', 'true'\)[\s\S]*aria-labelledby/, - 'app tray settings must expose dialog semantics'); -assert.match(trayManagerSource, /event\.key !== 'Tab'[\s\S]*focusable\(\)[\s\S]*event\.shiftKey/, - 'app tray settings must own a focus trap'); -assert.match(trayManagerSource, /overlayAcquired[\s\S]*if \(closed\)[\s\S]*releaseOverlay/, - 'app tray settings must release a late overlay acquisition'); const commandDom = new JSDOM(` @@ -757,6 +734,22 @@ assert.equal(JSON.stringify(creationCalls[0]), JSON.stringify(['Nova', { model: 'renderer creation must pass only the model override to the main process'); assert.equal(partialCreation.success, true, 'a persisted Agent must remain a successful creation result'); assert.equal(partialCreation.navigationSuccess, false, 'post-create UI failure must be reported separately'); +assert.match(partialCreation.warning, /尚未就绪/, + 'creation before provider readiness must expose a retryable terminal instead of reporting navigation success'); +const selectedItems = []; +const chatManagerProvider = { + selectItem: async (...args) => { selectedItems.push(args); }, + isReady: () => true, +}; +commandDom.window.dispatchEvent(new commandDom.window.CustomEvent('vcp-main-chat-commands-configure', { detail: { + chatManager: chatManagerProvider, + capabilities: {}, +} })); +commandDom.window.itemListManager.loadItems = async () => []; +const completeCreation = await commandDom.window.MainChatCommands.createAgent({ name: 'Terra', model: 'model-next' }); +assert.equal(completeCreation.navigationSuccess, true, + 'creation navigation must complete through the explicitly registered provider'); +assert.deepEqual(selectedItems[0], ['agent-1', 'agent', 'Terra', null, { model: 'model-next' }]); assert.match(mainHtml, /id="nextUiMainPanel"[^>]*>[\s\S]*
    [\s\S]*id="resizerRight"[\s\S]*id="notificationsSidebar"[\s\S]*<\/section>/s, 'main chat, notification resizer, and notification sidebar must share one clipping host'); @@ -887,9 +880,6 @@ const behaviorModal = VCPUI.create('Modal', { }); scope.append(behaviorModal.element); assert.equal(behaviorModal.element.querySelector('.vcp-ui-modal').getAttribute('role'), 'dialog'); -const behaviorDialog = behaviorModal.element.querySelector('.vcp-ui-modal'); -const behaviorTitleId = behaviorDialog.getAttribute('aria-labelledby'); -assert.ok(behaviorTitleId && behaviorDialog.querySelector(`#${behaviorTitleId}`), 'native modal must expose an accessible title'); behaviorModal.element.dispatchEvent(new dom.window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); assert.ok(!behaviorModal.element.isConnected, 'Escape must close the modal'); behaviorModal.close(null); diff --git a/scripts/write-chat-evidence-manifest.mjs b/scripts/write-chat-evidence-manifest.mjs new file mode 100644 index 000000000..b26e404ac --- /dev/null +++ b/scripts/write-chat-evidence-manifest.mjs @@ -0,0 +1,120 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const output = process.argv[2] ? path.resolve(root, process.argv[2]) : path.join(os.tmpdir(), `vcpchat-evidence-${Date.now()}.json`); +const readJson = relative => JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8')); +const readEvidence = (envName, label) => { + const configured = process.env[envName]; + if (!configured) return null; + const filename = path.resolve(configured); + if (!fs.existsSync(filename)) throw new Error(`${label} evidence path does not exist: ${filename}`); + let value; + try { value = JSON.parse(fs.readFileSync(filename, 'utf8')); } + catch (error) { throw new Error(`${label} evidence is not valid JSON: ${filename}`, { cause: error }); } + return { path: filename, value }; +}; +const mainChatEvidence = readEvidence('VCPCHAT_SEQUENCE_EVIDENCE_OUTPUT', 'main-chat'); +const uiAppsEvidence = readEvidence('VCPCHAT_UI_APPS_EVIDENCE_OUTPUT', 'UI Apps'); +const matrixEvidence = readEvidence('VCPCHAT_WINDOWS_MATRIX_EVIDENCE_INPUT', 'Windows matrix'); +const manualSoakEvidence = readEvidence('VCPCHAT_MANUAL_SOAK_EVIDENCE_INPUT', 'manual soak'); +const packagedEvidence = readEvidence('VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT', 'packaged artifact'); +const mainChatPass = mainChatEvidence?.value?.kind === 'electron-main-chat-sequence' + && mainChatEvidence.value.requiredEdges === '1/1' + && Array.isArray(mainChatEvidence.value.coverage?.missingRequiredEdges) + && mainChatEvidence.value.coverage.missingRequiredEdges.length === 0; +const uiAppsPass = uiAppsEvidence?.value?.kind === 'electron-ui-apps-smoke' + && Number.isInteger(uiAppsEvidence.value.total) + && uiAppsEvidence.value.total > 0 + && uiAppsEvidence.value.passed === uiAppsEvidence.value.total; +const matrixRows = matrixEvidence?.value?.rows; +const matrixPass = Array.isArray(matrixRows) && matrixRows.length > 0 && matrixRows.every(row => row.status === 'passed'); +const manualSoakStatus = manualSoakEvidence?.value?.status || 'manual_required'; +const packagedStatus = packagedEvidence?.value?.status || 'manual_required'; +if (matrixEvidence && !Array.isArray(matrixRows)) throw new Error('Windows matrix evidence must contain a rows array'); +if (manualSoakEvidence && !['manual_observation_required', 'passed'].includes(manualSoakStatus)) { + throw new Error(`manual soak evidence has invalid status: ${manualSoakStatus}`); +} +if (packagedEvidence && !['passed', 'failed', 'skipped'].includes(packagedStatus)) { + throw new Error(`packaged artifact evidence has invalid status: ${packagedStatus}`); +} +if (process.env.VCPCHAT_REAL_ELECTRON_STATUS === 'pass' && (!mainChatPass || !uiAppsPass)) { + throw new Error('VCPCHAT_REAL_ELECTRON_STATUS=pass requires valid passing main-chat and UI Apps evidence'); +} +if (process.env.VCPCHAT_REAL_ELECTRON_STATUS === 'pass') { + if (!packagedEvidence || packagedStatus !== 'passed') { + throw new Error(`VCPCHAT_REAL_ELECTRON_STATUS=pass requires packaged artifact status passed (received ${packagedStatus})`); + } + if (!matrixEvidence || !matrixPass) { + throw new Error('VCPCHAT_REAL_ELECTRON_STATUS=pass requires a complete passing Windows matrix'); + } + if (!manualSoakEvidence || manualSoakStatus !== 'passed') { + throw new Error(`VCPCHAT_REAL_ELECTRON_STATUS=pass requires manual soak status passed (received ${manualSoakStatus})`); + } +} +let commit = 'unknown'; +try { commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); } catch { /* source archives may not include git metadata */ } +const graph = readJson('docs/contracts/generated/chat-event-graph.json'); +const contracts = readJson('docs/contracts/chat-contracts.json'); +const manifest = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + commit, + node: process.version, + platform: process.platform, + arch: process.arch, + status: process.env.VCPCHAT_REAL_ELECTRON_STATUS === 'pass' ? 'pass' : 'manual_required', + checks: { + contracts: contracts.length, + generatedEvents: graph.events.length, + dynamicSites: { + registered: contracts.filter(contract => contract.dynamic === true).length, + graphRegistered: graph.registeredDynamic.length, + undiscovered: graph.undiscovered.length, + }, + transcriptSnapshots: contracts.filter(contract => contract.kind === 'snapshot').map(contract => contract.snapshot), + artifactPlane: 'electron-runtime-present', + realElectronSequence: mainChatEvidence ? (mainChatPass ? 'pass' : 'fail') : 'manual_required', + realElectronUiApps: uiAppsEvidence ? (uiAppsPass ? 'pass' : 'fail') : 'manual_required', + windowsMatrix: matrixEvidence ? (matrixPass ? 'pass' : 'fail_or_skipped') : 'manual_required', + manualSoak: manualSoakEvidence ? manualSoakStatus : 'manual_required', + packagedArtifact: packagedEvidence ? packagedStatus : 'manual_required', + }, + commands: [ + 'node scripts/check-chat-contracts.mjs', + 'node scripts/run-chat-contract-invariants.mjs', + 'node scripts/test-chat-contract-invalid.mjs', + 'node scripts/test-facade-registry-invalid.mjs', + 'node scripts/test-artifact-plane-invalid.mjs', + 'node scripts/test-packaged-artifact-invalid.mjs', + 'node scripts/check-artifact-plane.mjs', + 'node scripts/test-built-artifact-smoke.mjs', + 'node scripts/test-packaged-artifact-smoke.mjs', + 'node scripts/test-chat-transcript-snapshot.mjs', + ], + artifacts: { + graph: 'docs/contracts/generated/chat-event-graph.json', + contracts: 'docs/contracts/chat-contracts.json', + transcript: 'docs/contracts/snapshots/chat-stream.json', + nativeRuntime: `modules/services/chatDataService/bin/${process.platform}-${process.arch}`, + electronMainChat: mainChatEvidence?.path || null, + electronUiApps: uiAppsEvidence?.path || null, + windowsMatrix: matrixEvidence?.path || null, + manualSoak: manualSoakEvidence?.path || null, + packagedArtifact: packagedEvidence?.path || null, + }, + manualRequired: process.env.VCPCHAT_REAL_ELECTRON_STATUS === 'pass' ? [] : [ + 'test-electron-main-chat-sequences', + 'test-electron-ui-apps', + 'packaged Electron runtime launch/crash-reload', + 'cross-Windows/GPU/DPI matrix', + '30-60 minute manual soak', + ], + interpretation: 'This manifest records automated evidence only; manual_required and cross-platform D7 evidence remain separate.', +}; +fs.mkdirSync(path.dirname(output), { recursive: true }); +fs.writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); +console.log(`Chat evidence manifest written to ${output}`); diff --git a/styles/ui-next.css b/styles/ui-next.css index eeab290cc..84048dd61 100644 --- a/styles/ui-next.css +++ b/styles/ui-next.css @@ -1307,3 +1307,11 @@ body.light-theme .next-ui-nova-panel::before { drop-shadow(0 0 4px var(--next-empty-flow-glow-soft)); } } +html .vcp-standalone-chat { display: flex; flex-direction: column; min-height: 100%; padding: 24px; gap: 16px; } +html .vcp-standalone-chat > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; } +html .vcp-standalone-chat h1 { margin: 0; } +html .vcp-standalone-chat__status { margin: 4px 0 0; opacity: .7; } +html .vcp-standalone-chat__messages { flex: 1; min-height: 240px; overflow: auto; } +html .vcp-interactive-chat__composer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: end; } +html .vcp-interactive-chat__composer textarea { min-width: 0; resize: vertical; } +html .vcp-interactive-chat__composer [role="status"] { grid-column: 1 / -1; margin: 0; min-height: 1.25em; } diff --git a/tests/chat-context.test.mjs b/tests/chat-context.test.mjs new file mode 100644 index 000000000..369ba25b3 --- /dev/null +++ b/tests/chat-context.test.mjs @@ -0,0 +1,26 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatContext } from '../modules/chat/chatContext.js'; + +test('ChatContext exposes explicit state and ordered domain events', () => { + const context = createChatContext(); + const events = []; + const unsubscribe = context.subscribe(event => events.push(event.kind)); + context.setSelectedItem({ id: 'a', type: 'agent' }); + context.setTopicId('t1'); + context.setHistory([{ id: 'm1' }]); + assert.equal(context.selectedItem.id, 'a'); + assert.equal(context.topicId, 't1'); + assert.deepEqual(context.history, [{ id: 'm1' }]); + assert.deepEqual(events, ['selected-item', 'topic', 'history']); + unsubscribe(); + context.dispose(); +}); + +test('ChatContext does not expose mutable history storage', () => { + const context = createChatContext({ history: [{ id: 'm1' }] }); + context.history.push({ id: 'local-only' }); + assert.deepEqual(context.history, [{ id: 'm1' }]); + context.setHistory([{ id: 'm2' }]); + assert.deepEqual(context.history, [{ id: 'm2' }]); +}); diff --git a/tests/chat-dom-renderer.test.mjs b/tests/chat-dom-renderer.test.mjs new file mode 100644 index 000000000..18bddf733 --- /dev/null +++ b/tests/chat-dom-renderer.test.mjs @@ -0,0 +1,78 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createChatDomRenderer } from '../modules/chat/chatDomRenderer.js'; + +test('ChatDomRenderer delegates operations and rejects work after dispose', async () => { + const root = new JSDOM('
    ').window.document.querySelector('#root'); + const calls = []; + const adapter = createChatDomRenderer({ root, renderer: { + renderHistory: async () => { calls.push('history'); return 'ok'; }, + renderMessage: async () => { calls.push('message'); }, + renderMessageBatch: async () => { calls.push('batch'); }, + removeMessageById: () => calls.push('remove') + } }); + assert.equal(await adapter.renderHistory([]), 'ok'); + adapter.removeMessage('m1'); + adapter.dispose(); + await assert.rejects(() => adapter.renderMessage({}), /disposed/); + assert.deepEqual(calls, ['history', 'remove']); +}); + +test('ChatDomRenderer dispose reaches quiescence for an in-flight render', async () => { + const root = new JSDOM('
    ').window.document.querySelector('div'); + let resolveRender; + const adapter = createChatDomRenderer({ root, renderer: { + renderMessage: () => new Promise(resolve => { resolveRender = resolve; }) + } }); + const render = adapter.renderMessage({ id: 'm1' }); + let disposed = false; + const disposing = adapter.dispose().then(() => { disposed = true; }); + await Promise.resolve(); + assert.equal(disposed, false); + resolveRender('done'); + await disposing; + assert.equal(disposed, true); + await render; +}); + +test('ChatDomRenderer passes its root as render context', async () => { + const root = new JSDOM('
    ').window.document.querySelector('div'); + let receivedRoot; + const adapter = createChatDomRenderer({ root, renderer: { + renderMessage: async (...args) => { receivedRoot = args[4].root; } + } }); + await adapter.renderMessage({ id: 'm2' }); + assert.equal(receivedRoot, root); + await adapter.dispose(); +}); + +test('ChatDomRenderer forwards streaming messages without a hidden root side channel', async () => { + const root = new JSDOM('
    ').window.document.querySelector('div'); + let receivedMessage; + const adapter = createChatDomRenderer({ root, renderer: { + startStreamingMessage: async message => { receivedMessage = message; }, + } }); + await adapter.startStreaming({ id: 'stream-1' }); + assert.deepEqual(receivedMessage, { id: 'stream-1' }); + await adapter.dispose(); +}); + +test('ChatDomRenderer retracts owned routes before waiting for render work', async () => { + const root = new JSDOM('
    ').window.document.querySelector('div'); + const calls = []; + const adapter = createChatDomRenderer({ root, renderer: {} }); + adapter.own(() => calls.push('retract')); + await adapter.dispose(); + assert.deepEqual(calls, ['retract']); +}); + +test('ChatDomRenderer awaits failing and asynchronous owner disposal without starving later owners', async () => { + const root = new JSDOM('
    ').window.document.querySelector('div'); + const calls = []; + const adapter = createChatDomRenderer({ root, renderer: {} }); + adapter.own(async () => { await Promise.resolve(); calls.push('async'); }); + adapter.own(() => { calls.push('throw'); throw new Error('controlled'); }); + await adapter.dispose(); + assert.deepEqual(calls, ['throw', 'async']); +}); diff --git a/tests/chat-event-contract.test.mjs b/tests/chat-event-contract.test.mjs new file mode 100644 index 000000000..47c4ef110 --- /dev/null +++ b/tests/chat-event-contract.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + assertChatEventContract, + normalizeChatTerminal, + STREAM_TERMINAL_KINDS, +} from '../modules/chat/chatEventContract.js'; + +test('chat event contracts distinguish durable terminal from transient events', () => { + assert.equal(assertChatEventContract({ name: 'chat.stream.completed', terminal: true, durable: true }).terminal, true); + assert.equal(assertChatEventContract({ name: 'chat.stream.chunk', terminal: false, durable: false }).terminal, false); + assert.throws(() => assertChatEventContract({ name: 'chat.stream.completed', terminal: false, durable: true }), /terminal declaration mismatch/); +}); + +test('chat terminal normalization rejects unknown values at the contract seam', () => { + assert.deepEqual(normalizeChatTerminal('abort'), { kind: 'cancelled' }); + assert.deepEqual(normalizeChatTerminal({ type: 'error', message: 'x' }), { type: 'error', message: 'x', kind: 'failed' }); + assert.throws(() => normalizeChatTerminal('mystery'), /unknown chat stream terminal/); + assert.deepEqual(STREAM_TERMINAL_KINDS, ['completed', 'failed', 'cancelled', 'discarded']); +}); diff --git a/tests/chat-history-mutation-authority.test.mjs b/tests/chat-history-mutation-authority.test.mjs new file mode 100644 index 000000000..6ff295dcf --- /dev/null +++ b/tests/chat-history-mutation-authority.test.mjs @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatHistoryMutationAuthority } from '../modules/chat/chatHistoryMutationAuthority.js'; + +const deferred = () => { + let resolve; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +}; + +test('history mutations serialize per conversation and expose the durable commit result', async () => { + const firstSave = deferred(); + const calls = []; + const authority = createChatHistoryMutationAuthority({ + repository: { + async getHistory() { return []; }, + async saveHistory(_itemId, _itemType, topicId, history) { + calls.push({ topicId, history }); + if (calls.length === 1) await firstSave.promise; + return { success: true }; + }, + }, + }); + const first = authority.replace({ itemId: 'a', topicId: 't', category: 'outgoing' }, [{ id: '1' }]); + const second = authority.replace({ itemId: 'a', topicId: 't', category: 'attachment' }, [{ id: '2' }]); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(calls.length, 1); + firstSave.resolve(); + assert.equal((await first).category, 'outgoing'); + assert.equal((await second).category, 'attachment'); + assert.deepEqual(calls.map(call => call.history[0].id), ['1', '2']); +}); + +test('mutation reads inside its queue and disposal revokes new write authority', async () => { + let history = [{ id: '1' }]; + const authority = createChatHistoryMutationAuthority({ + repository: { + async getHistory() { return history; }, + async saveHistory(_itemId, _itemType, _topicId, next) { + history = next; + return { success: true }; + }, + }, + }); + await authority.mutate({ itemId: 'a', topicId: 't', category: 'edit' }, current => [...current, { id: '2' }]); + assert.deepEqual(history.map(message => message.id), ['1', '2']); + await authority.dispose(); + await assert.rejects( + authority.replace({ itemId: 'a', topicId: 't' }, []), + /disposed/ + ); +}); diff --git a/tests/chat-history-persistence.test.mjs b/tests/chat-history-persistence.test.mjs new file mode 100644 index 000000000..6c03cab3c --- /dev/null +++ b/tests/chat-history-persistence.test.mjs @@ -0,0 +1,75 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatHistoryPersistence, createTransientChatHistoryPersistence } from '../modules/chat/chatHistoryPersistence.js'; + +const projected = history => ({ + messageId: 'assistant-a', + context: { agentId: 'agent', topicId: 'topic' }, + content: 'answer a', + finishReason: 'completed', + history, +}); + +test('terminal persistence merges reverse-completing messages by identity and chronological order', async () => { + let durable = [ + { id: 'user-a', role: 'user', content: 'a', timestamp: 1 }, + { id: 'user-b', role: 'user', content: 'b', timestamp: 3 }, + { id: 'assistant-b', role: 'assistant', content: 'answer b', timestamp: 4 }, + ]; + const persistence = createChatHistoryPersistence({ + async getHistory() { return durable.map(message => ({ ...message })); }, + async saveHistory(_itemId, _itemType, _topicId, history) { durable = history; }, + }); + + await persistence.commit(projected([ + { id: 'user-a', role: 'user', content: 'a', timestamp: 1 }, + { id: 'assistant-a', role: 'assistant', content: 'answer a', timestamp: 2 }, + { id: 'user-b', role: 'user', content: 'b', timestamp: 3 }, + { id: 'pending', role: 'assistant', isPendingStream: true, timestamp: 5 }, + ])); + + assert.deepEqual(durable.map(message => message.id), ['user-a', 'assistant-a', 'user-b', 'assistant-b']); +}); + +test('terminal persistence propagates read and write failures without a fallback overwrite', async () => { + const readFailure = createChatHistoryPersistence({ + async getHistory() { throw new Error('read failed'); }, + async saveHistory() { assert.fail('save must not run after a failed durable read'); }, + }); + await assert.rejects(readFailure.commit(projected([])), /read failed/); + + const writeFailure = createChatHistoryPersistence({ + async getHistory() { return []; }, + async saveHistory() { throw new Error('write failed'); }, + }); + await assert.rejects(writeFailure.commit(projected([])), /write failed/); +}); + +test('temporary assistant and group projections do not write renderer-owned history', async () => { + let writes = 0; + const persistence = createChatHistoryPersistence({ + async getHistory() { return []; }, + async saveHistory() { writes += 1; }, + }); + await persistence.commit({ ...projected([]), context: { agentId: 'a', topicId: 'assistant_chat' } }); + await persistence.commit({ ...projected([]), context: { groupId: 'g', topicId: 't', isGroupMessage: true } }); + assert.equal(writes, 0); +}); + +test('auxiliary session persistence updates transient history without claiming durability', async () => { + let sessionHistory = [{ id: 'user-a', role: 'user', content: 'question', timestamp: 1 }]; + const persistence = createTransientChatHistoryPersistence({ + async getHistory() { return sessionHistory; }, + async saveHistory(_itemId, _itemType, _topicId, history) { sessionHistory = history; }, + }); + + assert.equal(persistence.durable, false); + await persistence.commit({ + messageId: 'assistant-a', + finishReason: 'completed', + content: 'answer', + context: { agentId: 'a', topicId: 'assistant_chat' }, + history: [...sessionHistory, { id: 'assistant-a', role: 'assistant', content: 'answer', timestamp: 2 }], + }); + assert.deepEqual(sessionHistory.map(message => message.id), ['user-a', 'assistant-a']); +}); diff --git a/tests/chat-manager-selection-race.test.js b/tests/chat-manager-selection-race.test.js index e6f2dc6e1..1489fb5a8 100644 --- a/tests/chat-manager-selection-race.test.js +++ b/tests/chat-manager-selection-race.test.js @@ -10,7 +10,7 @@ function deferred() { return { promise, resolve, reject }; } -function createFixture() { +function createFixture(options = {}) { const dom = new JSDOM(`
    @@ -23,13 +23,20 @@ function createFixture() {
      `, { runScripts: 'outside-only', url: 'https://vcpchat.local/' }); const { window } = dom; - window.eval(fs.readFileSync('modules/chatManager.js', 'utf8')); + window.eval(`${fs.readFileSync('modules/chatManager.js', 'utf8').replace(/\bexport\s+(?=const\s+chatManager\b)/, '')}\nwindow.__testChatManager = chatManager;`); + window.chatManager = window.__testChatManager; - let selected = { id: null, type: null, name: null, avatarUrl: null, config: null }; + let selected = Object.freeze({ id: null, type: null, name: null, avatarUrl: null, config: null }); let topicId = null; let history = []; let attachedFiles = []; + const attachmentRef = { + get: () => attachedFiles, + set: value => { attachedFiles = value; }, + append: value => { attachedFiles = [...attachedFiles, value]; return attachedFiles; }, + }; let nextHistorySaveGate = null; + let nextHistoryReadGate = null; let nextHistorySaveError = null; let nextWatcherStartError = null; let nextVcpError = null; @@ -50,6 +57,9 @@ function createFixture() { 'agent-b': { id: 'agent-b', name: 'Agent B', agentDataPath: '/tmp/b', topics: [{ id: 'topic-b', createdAt: 2 }, { id: 'topic-b-2', createdAt: 3 }] }, }; const chatMessages = window.document.getElementById('chatMessages'); + let canvasContentListener = null; + let canvasClosedListener = null; + let canvasDisposals = 0; const messageRenderer = { setCurrentSelectedItem() {}, setCurrentTopicId() {}, setCurrentItemAvatar() {}, setCurrentItemAvatarColor() {}, clearChat() { chatMessages.textContent = ''; }, @@ -75,8 +85,15 @@ function createFixture() { }, }; const electronAPI = { - onCanvasContentUpdate() {}, - onCanvasWindowClosed() {}, + onCanvasContentUpdate(listener) { + if (options.failCanvasRegistration) throw new Error('controlled canvas listener failure'); + canvasContentListener = listener; + return () => { canvasDisposals += 1; }; + }, + onCanvasWindowClosed(listener) { + canvasClosedListener = listener; + return () => { canvasDisposals += 1; }; + }, watcherStop: async () => {}, watcherStart: async (_path, itemId, requestedTopicId) => { if (nextWatcherStartError) { @@ -92,7 +109,15 @@ function createFixture() { topicRequests.set(itemId, pending); return pending.promise; }, - getChatHistory: async (itemId, requestedTopicId) => histories.get(`${itemId}:${requestedTopicId}`) || [], + getChatHistory: async (itemId, requestedTopicId) => { + const gate = nextHistoryReadGate; + if (gate) { + nextHistoryReadGate = null; + gate.started.resolve(); + await gate.release.promise; + } + return histories.get(`${itemId}:${requestedTopicId}`) || []; + }, getAgentConfig: async itemId => configs[itemId], createNewTopicForAgent: itemId => { const pending = deferred(); @@ -138,11 +163,18 @@ function createFixture() { return { success: true }; }, }; - window.chatManager.init({ + let initError = null; + try { + window.chatManager.init({ + chatRepository: { + getHistory: (itemId, _itemType, requestedTopicId) => electronAPI.getChatHistory(itemId, requestedTopicId), + saveHistory: (itemId, _itemType, requestedTopicId, messages) => electronAPI.saveChatHistory(itemId, requestedTopicId, messages), + }, electronAPI, uiHelper: { showToastNotification() {}, autoResizeTextarea() {}, openModal() {} }, modules: { messageRenderer, + streamManager: options.streamProjection || null, itemListManager: { highlightActiveItem() {}, refreshUnreadCounts() {}, @@ -151,14 +183,14 @@ function createFixture() { return item && itemType === 'agent' ? { ...item, type: 'agent' } : null; }, }, - topicListManager: { loadTopicList() {} }, + topicListManager: { loadTopicList: () => options.topicListProjection?.promise }, groupRenderer: null, }, refs: { - currentSelectedItemRef: { get: () => selected, set: value => { selected = value; } }, + currentSelectedItemRef: { get: () => selected, set: value => { selected = Object.freeze({ ...value }); } }, currentTopicIdRef: { get: () => topicId, set: value => { topicId = value; } }, currentChatHistoryRef: { get: () => history, set: value => { history = value; } }, - attachedFilesRef: { get: () => attachedFiles, set: value => { attachedFiles = value; } }, + attachedFilesRef: attachmentRef, globalSettingsRef: { get: () => ({ assistantEnabled: false, vcpServerUrl: 'http://fixture.local/v1/chat', vcpApiKey: 'test' }) }, }, elements: { @@ -170,21 +202,31 @@ function createFixture() { attachFileBtn: window.document.getElementById('attachFileBtn'), }, mainRendererFunctions: { displaySettingsForItem() {}, updateAttachmentPreview() {} }, - }); + }); + } catch (error) { + initError = error; + } return { dom, window, chatManager: window.chatManager, + initError, topicRequests, createTopicRequests, watcherRequests, sentRequests, + attachmentRef, configs, holdNextHistorySave() { const gate = { started: deferred(), release: deferred() }; nextHistorySaveGate = gate; return gate; }, + holdNextHistoryRead() { + const gate = { started: deferred(), release: deferred() }; + nextHistoryReadGate = gate; + return gate; + }, failNextHistorySave(message = 'controlled save failure') { nextHistorySaveError = new Error(message); }, @@ -201,9 +243,20 @@ function createFixture() { }, savedSettings, historySaveCount: () => historySaveCount, + canvas: { + emitContent: value => canvasContentListener?.(value), + emitClosed: () => canvasClosedListener?.(), + disposalCount: () => canvasDisposals, + }, persistedHistory(itemId, requestedTopicId) { return JSON.parse(JSON.stringify(histories.get(`${itemId}:${requestedTopicId}`) || [])); }, + setPersistedHistory(itemId, requestedTopicId, messages) { + histories.set(`${itemId}:${requestedTopicId}`, JSON.parse(JSON.stringify(messages))); + }, + setCurrentHistory(messages) { + history = messages; + }, state: () => ({ selected, topicId, @@ -211,12 +264,19 @@ function createFixture() { ? window.localStorage.getItem(`lastActiveTopic_${selected.id}_${selected.type}`) : null, history: [...history], + attachedFiles: [...attachedFiles], visibleMessageIds: [...chatMessages.querySelectorAll('.message-item:not(.topic-timestamp-bubble)')] .map(element => element.dataset.messageId), }), }; } +test('ChatManager publishes readiness only after synchronous listener setup completes', () => { + const fixture = createFixture({ failCanvasRegistration: true }); + assert.match(fixture.initError?.message || '', /controlled canvas listener failure/); + assert.equal(fixture.chatManager.isReady(), false); +}); + test('a late assistant selection cannot overwrite the newer assistant topic and history', async () => { const fixture = createFixture(); const selectA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); @@ -255,6 +315,28 @@ test('startup restoration selects the last durable assistant and topic', async ( fixture.dom.window.close(); }); +test('startup restoration waits for the topic projection consumer', async () => { + const topicListProjection = deferred(); + const fixture = createFixture({ topicListProjection }); + let restored = false; + const restoring = fixture.chatManager.restoreLastOpenState({ + lastOpenItemId: 'agent-b', + lastOpenItemType: 'agent', + lastOpenTopicId: 'topic-b-2', + }).then(result => { + restored = result; + return result; + }); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-b').resolve(fixture.configs['agent-b'].topics); + await new Promise(resolve => setImmediate(resolve)); + + assert.equal(restored, false, 'restoration resolved before the topic projection completed'); + topicListProjection.resolve(); + assert.equal(await restoring, true); + fixture.dom.window.close(); +}); + test('an explicit selection supersedes an unfinished startup restoration', async () => { const fixture = createFixture(); const restoring = fixture.chatManager.restoreLastOpenState({ @@ -386,6 +468,38 @@ test('a failed durable save preserves the draft and retracts the optimistic mess fixture.dom.window.close(); }); +test('an attachment added while the outgoing message is persisting remains in the draft', async () => { + const fixture = createFixture(); + const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-a').resolve(fixture.configs['agent-a'].topics); + await selected; + + const firstAttachment = { + file: { name: 'sent.txt', type: 'text/plain', size: 4 }, + originalName: 'sent.txt', + localPath: '/tmp/sent.txt', + }; + fixture.attachmentRef.append(firstAttachment); + fixture.window.document.getElementById('messageInput').value = 'send with attachment'; + const saveGate = fixture.holdNextHistorySave(); + const sending = fixture.chatManager.handleSendMessage(); + await saveGate.started.promise; + + const lateAttachment = { + file: { name: 'next.txt', type: 'text/plain', size: 4 }, + originalName: 'next.txt', + localPath: '/tmp/next.txt', + }; + fixture.attachmentRef.append(lateAttachment); + saveGate.release.resolve(); + await sending; + + assert.deepEqual(fixture.state().attachedFiles, [firstAttachment, lateAttachment]); + assert.equal(fixture.window.document.getElementById('messageInput').value, 'send with attachment'); + fixture.dom.window.close(); +}); + test('failed VCP startup does not rewrite history to remove an unpersisted thinking message', async () => { const fixture = createFixture(); const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); @@ -521,3 +635,128 @@ test('a deletion completed for an old assistant cannot rewrite the new assistant assert.deepEqual(state.history.map(message => message.id), ['b-message']); fixture.dom.window.close(); }); + +test('ChatManager dispose retracts Canvas subscriptions and ignores late Canvas events', async () => { + const fixture = createFixture(); + const input = fixture.window.document.getElementById('messageInput'); + fixture.canvas.emitContent({ content: 'before-dispose' }); + assert.match(input.value, /VCPChatCanvas/); + input.value = 'stable'; + + await fixture.chatManager.dispose(); + await fixture.chatManager.dispose(); + fixture.canvas.emitContent({ content: 'late' }); + fixture.canvas.emitClosed(); + + assert.equal(fixture.canvas.disposalCount(), 2); + assert.equal(input.value, 'stable'); + assert.equal(fixture.chatManager.isReady(), false); + fixture.dom.window.close(); +}); + +test('a history-file sync resolving after selection changes cannot mutate the new conversation', async () => { + const fixture = createFixture(); + const selectedB = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-b').resolve(fixture.configs['agent-b'].topics); + await selectedB; + + fixture.setPersistedHistory('agent-b', 'topic-b', [ + { id: 'late-b-message', role: 'assistant', content: 'Late B', timestamp: 4 }, + ]); + const historyRead = fixture.holdNextHistoryRead(); + const sync = fixture.chatManager.syncHistoryFromFile('agent-b', 'agent', 'topic-b'); + await historyRead.started.promise; + + const selectedA = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-a').resolve(fixture.configs['agent-a'].topics); + await selectedA; + historyRead.release.resolve(); + await sync; + + const state = fixture.state(); + assert.equal(state.selected.id, 'agent-a'); + assert.equal(state.topicId, 'topic-a'); + assert.deepEqual(state.history.map(message => message.id), ['a-message']); + assert.deepEqual(state.visibleMessageIds.filter(Boolean), ['a-message']); + fixture.dom.window.close(); +}); + +test('a history-file sync resolving after dispose cannot mutate history or the DOM', async () => { + const fixture = createFixture(); + const selected = fixture.chatManager.selectItem('agent-b', 'agent', 'Agent B', null, fixture.configs['agent-b']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-b').resolve(fixture.configs['agent-b'].topics); + await selected; + + fixture.setPersistedHistory('agent-b', 'topic-b', [ + { id: 'late-b-message', role: 'assistant', content: 'Late B', timestamp: 4 }, + ]); + const historyRead = fixture.holdNextHistoryRead(); + const sync = fixture.chatManager.syncHistoryFromFile('agent-b', 'agent', 'topic-b'); + await historyRead.started.promise; + const beforeDispose = fixture.state(); + const disposal = fixture.chatManager.dispose(); + historyRead.release.resolve(); + await Promise.all([sync, disposal]); + + const state = fixture.state(); + assert.deepEqual(state.history, beforeDispose.history); + assert.deepEqual(state.visibleMessageIds, beforeDispose.visibleMessageIds); + fixture.dom.window.close(); +}); + +test('history load reconciles an active stream after replacing the Surface projection', async () => { + let reconcileCalls = []; + const fixture = createFixture({ + streamProjection: { + async reconcileConversation(identity) { + reconcileCalls.push(identity); + return []; + }, + }, + }); + const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-a').resolve(fixture.configs['agent-a'].topics); + await selected; + + assert.equal(reconcileCalls.length, 1); + assert.equal(reconcileCalls[0].itemType, 'agent'); + assert.equal(reconcileCalls[0].itemId, 'agent-a'); + assert.equal(reconcileCalls[0].topicId, 'topic-a'); + fixture.dom.window.close(); +}); + +test('history sync does not delete a terminal message that commits while the file read is in flight', async () => { + let active = true; + const fixture = createFixture({ + streamProjection: { + getActiveStreamingMessageId: () => active ? 'tool-terminal' : null, + }, + }); + const selected = fixture.chatManager.selectItem('agent-a', 'agent', 'Agent A', null, fixture.configs['agent-a']); + await new Promise(resolve => setImmediate(resolve)); + fixture.topicRequests.get('agent-a').resolve(fixture.configs['agent-a'].topics); + await selected; + + fixture.setPersistedHistory('agent-a', 'topic-a', [ + { id: 'a-message', role: 'assistant', content: 'A', timestamp: 1 }, + ]); + const historyRead = fixture.holdNextHistoryRead(); + const sync = fixture.chatManager.syncHistoryFromFile('agent-a', 'agent', 'topic-a'); + await historyRead.started.promise; + + // Simulate the terminal commit arriving after the file read began. + active = false; + fixture.setCurrentHistory([ + ...fixture.state().history, + { id: 'tool-terminal', role: 'assistant', content: 'tool result', timestamp: 2 }, + ]); + historyRead.release.resolve(); + await sync; + + assert.ok(fixture.state().history.some(message => message.id === 'tool-terminal')); + fixture.dom.window.close(); +}); diff --git a/tests/chat-operation.test.mjs b/tests/chat-operation.test.mjs new file mode 100644 index 000000000..8f34c2506 --- /dev/null +++ b/tests/chat-operation.test.mjs @@ -0,0 +1,42 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatOperations } from '../modules/chat/chatOperation.js'; + +test('ChatOperations uses real send promise and locks duplicate submissions', async () => { + let resolve; + const operations = createChatOperations({ send: () => new Promise(r => { resolve = r; }) }); + const pending = operations.sendMessage({ content: 'hello' }); + assert.equal(operations.state.status, 'pending'); + await assert.rejects(() => operations.sendMessage({ content: 'duplicate' }), /already pending/); + resolve({ success: true }); + assert.deepEqual(await pending, { success: true }); + assert.equal(operations.state.status, 'idle'); +}); + +test('ChatOperations dispose cancels and waits for the operation', async () => { + let resolve; + let cancelled = false; + const operations = createChatOperations({ send: () => new Promise(r => { resolve = r; }), cancel: async () => { cancelled = true; resolve?.({ cancelled: true }); } }); + operations.sendMessage({ content: 'hello' }); + await operations.dispose(); + assert.equal(cancelled, true); + assert.equal(operations.state.status, 'disposed'); +}); + +test('ChatOperations cancellation is idempotent and late completion cannot regain authority', async () => { + let resolve; + let cancelCalls = 0; + const operations = createChatOperations({ + send: () => new Promise(r => { resolve = r; }), + cancel: async () => { cancelCalls += 1; } + }); + const pending = operations.sendMessage({ content: 'hold' }); + assert.equal(await operations.cancel(), true); + assert.equal(await operations.cancel(), true); + resolve({ cancelled: true }); + await pending; + assert.equal(operations.state.status, 'idle'); + assert.equal(cancelCalls, 1); + await operations.dispose(); + assert.equal(operations.state.status, 'disposed'); +}); diff --git a/tests/chat-plugin-manifest.test.mjs b/tests/chat-plugin-manifest.test.mjs new file mode 100644 index 000000000..f43ba2e3b --- /dev/null +++ b/tests/chat-plugin-manifest.test.mjs @@ -0,0 +1,106 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatPluginLoader, validateChatPluginManifest } from '../modules/chat/chatPluginManifest.js'; + +test('chat plugin manifest rejects undeclared powers and loader disposes provider', () => { + assert.throws(() => validateChatPluginManifest({ id: 'bad', apiVersion: 1, cssText: 'body{}' }), /CSS text/); + assert.throws(() => validateChatPluginManifest({ id: 'bad', apiVersion: 1, selectors: ['.message-item'] }), /selectors/); + assert.throws(() => validateChatPluginManifest({ id: 'bad', apiVersion: 1, ipc: ['save-chat'] }), /IPC/); + assert.throws(() => validateChatPluginManifest({ id: 'bad', apiVersion: 1, capabilities: ['history-write'] }), /unsupported chat plugin capability/); + assert.throws(() => validateChatPluginManifest({ id: 'bad', apiVersion: 1, slots: ['arbitrary-root'] }), /unsupported chat plugin slot/); + let disposed = false; + const loader = createChatPluginLoader({ state: { getSnapshot: () => ({ mode: 'idle' }) }, slots: { register: () => () => {} } }); + const uninstall = loader.install({ id: 'skin', apiVersion: 1, capabilities: ['theme'], slots: [] }, ({ manifest, state }) => { + assert.equal(manifest.id, 'skin'); + assert.equal(state.mode, 'idle'); + return () => { disposed = true; }; + }); + uninstall(); + assert.equal(disposed, true); + loader.dispose(); +}); + +test('chat plugin loader owns slot/theme/skin teardown and rejects undeclared capability', () => { + const events = []; + const slots = { register: (_slot, _id, _mount) => () => events.push('slot') }; + const loader = createChatPluginLoader({ + state: { getSnapshot: () => ({ mode: 'next' }) }, slots + }); + const uninstall = loader.install({ id: 'controlled', apiVersion: 1, capabilities: ['surface-slot'], slots: ['header'] }, ({ registerSlot, applyTheme }) => { + registerSlot('header', 'x', () => {}); + assert.throws(() => applyTheme({}, { apply: () => {} }), /theme capability/); + return () => events.push('provider'); + }); + uninstall(); + assert.deepEqual(events, ['slot', 'provider']); + loader.dispose(); + assert.throws(() => loader.install({ id: 'late', apiVersion: 1, capabilities: [], slots: [] }, () => {}), /disposed/); +}); + +test('chat plugin state subscription is capability-gated, readonly, and disposed with owner', () => { + let active = true; + const snapshots = []; + const listeners = new Set(); + const state = { + getSnapshot: () => ({ mode: 'idle' }), + subscribe(listener) { listeners.add(listener); listener({ mode: 'idle' }); return () => { active = false; listeners.delete(listener); }; } + }; + const loader = createChatPluginLoader({ state }); + loader.install({ id: 'stateful', apiVersion: 1, capabilities: ['presentation-state'], slots: [] }, ({ subscribeState }) => { + subscribeState(snapshot => { + snapshots.push(snapshot); + assert.throws(() => { snapshot.mode = 'mutated'; }, TypeError); + }); + }); + assert.equal(snapshots.length, 1); + loader.dispose(); + assert.equal(active, false); + assert.throws(() => createChatPluginLoader({ state }).install({ id: 'plain', apiVersion: 1, capabilities: [], slots: [] }, ({ subscribeState }) => subscribeState(() => {})), /capability/); +}); + +test('loader owns presentation skin updates and removes the subscription on uninstall', () => { + const listeners = new Set(); + const state = { + getSnapshot: () => ({ mode: 'idle' }), + subscribe(listener) { listeners.add(listener); listener({ mode: 'idle' }); return () => listeners.delete(listener); } + }; + const root = { dataset: {} }; + const loader = createChatPluginLoader({ state }); + const seen = []; + const uninstall = loader.install({ id: 'updates', apiVersion: 1, capabilities: ['presentation-state'], slots: [] }, ({ mountSkin }) => { + const skin = { mount(host, snapshot) { host.dataset.mode = snapshot.mode; const teardown = () => {}; teardown.update = next => { seen.push(next.mode); host.dataset.mode = next.mode; }; return teardown; } }; + mountSkin(root, skin); + }); + [...listeners][0]({ mode: 'streaming' }); + assert.deepEqual(seen, ['idle', 'streaming']); + uninstall(); + assert.equal(listeners.size, 0); +}); + +test('plugin provider failure rolls back every earlier registration', () => { + const events = []; + const loader = createChatPluginLoader({ + slots: { register: () => () => events.push('slot') } + }); + assert.throws(() => loader.install({ id: 'rollback', apiVersion: 1, capabilities: ['surface-slot'], slots: ['header'] }, ({ registerSlot }) => { + registerSlot('header', 'first', () => {}); + throw new Error('controlled provider failure'); + }), /controlled provider failure/); + assert.deepEqual(events, ['slot']); + assert.doesNotThrow(() => loader.install({ id: 'rollback', apiVersion: 1, capabilities: [], slots: [] }, () => {})); +}); + +test('uninstalled plugin ignores late state updates and duplicate uninstall', () => { + const listeners = new Set(); + const state = { getSnapshot: () => ({ mode: 'idle' }), subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } }; + const loader = createChatPluginLoader({ state }); + let updates = 0; + const uninstall = loader.install({ id: 'late-state', apiVersion: 1, capabilities: ['presentation-state'], slots: [] }, ({ subscribeState }) => { + subscribeState(() => { updates += 1; }); + }); + uninstall(); + uninstall(); + for (const listener of listeners) listener({ mode: 'late' }); + assert.equal(updates, 0); + assert.equal(listeners.size, 0); +}); diff --git a/tests/chat-presentation-skin.test.mjs b/tests/chat-presentation-skin.test.mjs new file mode 100644 index 000000000..e9d1ffed4 --- /dev/null +++ b/tests/chat-presentation-skin.test.mjs @@ -0,0 +1,35 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createPresentationSkin } from '../modules/chat/chatPresentationSkin.js'; + +test('presentation skin receives only frozen state/tokens and disposes owned DOM', () => { + const root = new JSDOM('
      ').window.document.querySelector('div'); + let disposed = false; + const skin = createPresentationSkin({ id: 'readonly-badge', tokens: { accent: '#888' }, render: (host, state, tokens) => { + host.dataset.mode = state.mode; + host.dataset.accent = tokens.accent; + return () => { disposed = true; delete host.dataset.mode; }; + } }); + const teardown = skin.mount(root, { mode: 'readonly' }); + assert.equal(root.dataset.mode, 'readonly'); + assert.equal(Object.isFrozen(skin.tokens), true); + teardown(); + assert.equal(disposed, true); + assert.equal(root.dataset.mode, undefined); +}); + +test('presentation skin update is state-owned and unsubscribed with the mount', () => { + const root = new JSDOM('
      ').window.document.querySelector('div'); + const seen = []; + const skin = createPresentationSkin({ + id: 'stateful', + render: (host, state) => { host.dataset.mode = state.mode; return () => {}; }, + update: (host, state) => { seen.push(state.mode); host.dataset.mode = state.mode; } + }); + const mounted = skin.mount(root, { mode: 'idle' }); + mounted.update({ mode: 'streaming' }); + assert.deepEqual(seen, ['streaming']); + assert.equal(root.dataset.mode, 'streaming'); + mounted(); +}); diff --git a/tests/chat-presentation-state.test.mjs b/tests/chat-presentation-state.test.mjs new file mode 100644 index 000000000..21df06f24 --- /dev/null +++ b/tests/chat-presentation-state.test.mjs @@ -0,0 +1,27 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatPresentationState } from '../modules/chat/chatPresentationState.js'; + +test('presentation state publishes readonly snapshots and quiesces on dispose', () => { + const state = createChatPresentationState({ theme: 'dark' }); + const seen = []; + const unsubscribe = state.subscribe(snapshot => seen.push(snapshot)); + state.set({ mode: 'streaming', activeSurface: 'standalone' }); + assert.equal(seen[0].theme, 'dark'); + assert.equal(seen[1].mode, 'streaming'); + try { seen[1].mode = 'mutated'; } catch {} + assert.equal(state.getSnapshot().mode, 'streaming'); + state.dispose(); + state.set({ mode: 'error' }); + unsubscribe(); + assert.equal(seen.length, 2); +}); + +test('one presentation subscriber cannot abort the state transition for other consumers', () => { + const state = createChatPresentationState(); + const seen = []; + state.subscribe(() => { throw new Error('broken skin'); }); + state.subscribe(snapshot => seen.push(snapshot.mode)); + assert.doesNotThrow(() => state.set({ mode: 'streaming' })); + assert.deepEqual(seen, ['idle', 'streaming']); +}); diff --git a/tests/chat-repository.test.mjs b/tests/chat-repository.test.mjs new file mode 100644 index 000000000..a14c2e9c2 --- /dev/null +++ b/tests/chat-repository.test.mjs @@ -0,0 +1,18 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChatRepository } from '../modules/chat/chatRepository.js'; + +test('ChatRepository maps agent and group history through the narrow contract', async () => { + const calls = []; + const repo = createChatRepository({ + getChatHistory: async (...args) => { calls.push(['agent-get', ...args]); return []; }, + getGroupChatHistory: async (...args) => { calls.push(['group-get', ...args]); return []; }, + saveChatHistory: async (...args) => { calls.push(['agent-save', ...args]); return { success: true }; }, + saveGroupChatHistory: async (...args) => { calls.push(['group-save', ...args]); return { success: true }; } + }); + await repo.getHistory('a', 'agent', 't'); + await repo.getHistory('g', 'group', 't'); + await repo.saveHistory('a', 'agent', 't', []); + await repo.saveHistory('g', 'group', 't', []); + assert.deepEqual(calls.map(([kind]) => kind), ['agent-get', 'group-get', 'agent-save', 'group-save']); +}); diff --git a/tests/chat-surface-slots.test.mjs b/tests/chat-surface-slots.test.mjs new file mode 100644 index 000000000..6655d17df --- /dev/null +++ b/tests/chat-surface-slots.test.mjs @@ -0,0 +1,29 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createChatSurfaceSlots } from '../modules/chat/chatSurfaceSlots.js'; + +test('chat surface slots are named, disposable and receive readonly snapshots', () => { + const root = new JSDOM('
      ').window.document.querySelector('div'); + const slots = createChatSurfaceSlots(); + const seen = []; + const dispose = slots.register('header', 'skin', (host, snapshot) => { seen.push(snapshot.mode); host.textContent = 'skin'; }); + const owned = slots.mount('header', root, { mode: 'readonly', canSend: false }); + assert.deepEqual(seen, ['readonly']); + assert.equal(root.querySelector('[data-chat-slot-owner="skin"]')?.textContent, 'skin'); + owned.forEach(unmount => unmount()); + dispose(); + slots.dispose(); + assert.equal(root.childElementCount, 0); +}); + +test('slot mount rolls back earlier contributions when a later consumer fails', () => { + const root = new JSDOM('
      ').window.document.querySelector('div'); + const slots = createChatSurfaceSlots(); + let disposed = 0; + slots.register('header', 'first', () => () => { disposed += 1; }); + slots.register('header', 'broken', () => { throw new Error('mount failed'); }); + assert.throws(() => slots.mount('header', root, {}), /mount failed/); + assert.equal(root.childElementCount, 0); + assert.equal(disposed, 1); +}); diff --git a/tests/chat-surface.test.mjs b/tests/chat-surface.test.mjs new file mode 100644 index 000000000..013b10e35 --- /dev/null +++ b/tests/chat-surface.test.mjs @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createChatSurface, createReadOnlyChatSurface } from '../modules/chat/chatSurface.js'; + +test('read-only ChatSurface ignores a late history result after dispose', async () => { + const dom = new JSDOM('
      '); + let resolveHistory; + const renderer = { renderHistory: async () => { throw new Error('must not render after dispose'); } }; + const surface = createReadOnlyChatSurface({ + root: dom.window.document.querySelector('#root'), + renderer, + repository: { getHistory: () => new Promise(resolve => { resolveHistory = resolve; }) } + }); + const load = surface.loadHistory('a', 'agent', 't'); + const firstDispose = surface.dispose(); + const secondDispose = surface.dispose(); + resolveHistory([]); + assert.deepEqual(await load, { stale: true }); + await Promise.all([firstDispose, secondDispose]); + assert.equal(surface.disposed, true); +}); + +test('interactive ChatSurface drains its real operation before renderer teardown', async () => { + const dom = new JSDOM('
      '); + const sequence = []; + let settleOperation; + const operationDone = new Promise(resolve => { settleOperation = resolve; }); + const surface = createChatSurface({ + root: dom.window.document.querySelector('#root'), + renderer: {}, + repository: { getHistory: async () => [] }, + mode: 'interactive', + operations: { + async dispose() { + sequence.push('operation-cancel'); + settleOperation(); + await operationDone; + sequence.push('operation-terminal'); + }, + }, + disposeRenderer: async () => { + sequence.push('renderer-dispose'); + assert.deepEqual(sequence, ['operation-cancel', 'operation-terminal', 'renderer-dispose']); + }, + }); + + await surface.dispose(); + + assert.deepEqual(sequence, ['operation-cancel', 'operation-terminal', 'renderer-dispose']); + dom.window.close(); +}); diff --git a/tests/chat-theme-plugin.test.mjs b/tests/chat-theme-plugin.test.mjs new file mode 100644 index 000000000..cf5e0f7aa --- /dev/null +++ b/tests/chat-theme-plugin.test.mjs @@ -0,0 +1,14 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createChatThemePlugin } from '../modules/chat/chatThemePlugin.js'; + +test('chat theme plugin applies only allowlisted tokens and disposes them', () => { + const root = new JSDOM('
      ').window.document.querySelector('div'); + const plugin = createChatThemePlugin({ id: 'soft', tokens: { accent: '#abc', surface: '#fff' } }); + const dispose = plugin.apply(root); + assert.equal(root.style.getPropertyValue('--chat-accent'), '#abc'); + dispose(); + assert.equal(root.style.getPropertyValue('--chat-accent'), ''); + assert.throws(() => createChatThemePlugin({ id: 'bad', tokens: { css: 'body{}' } }), /Unsupported/); +}); diff --git a/tests/content-pipeline.test.mjs b/tests/content-pipeline.test.mjs new file mode 100644 index 000000000..47685d716 --- /dev/null +++ b/tests/content-pipeline.test.mjs @@ -0,0 +1,48 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createContentPipeline, PIPELINE_MODES } from '../modules/renderer/contentPipeline.js'; + +test('content pipeline keeps thought, tool, request and code protocols ordered and isolated', () => { + const pipeline = createContentPipeline({ + getToolResultRegex: () => /\[RESULT:[\s\S]*?\]/g, + getToolRequestRegex: () => /<<<\[TOOL_REQUEST\]>>>[\s\S]*?<<<\[END_TOOL_REQUEST\]>>>/g, + getCodeFenceRegex: () => /```[\s\S]*?```/g, + processStartEndMarkers: value => value.replace('「始」', '').replace('「末」', '') + }); + const input = [ + '\nprivate reasoning\n', + '[RESULT: **raw tool output**]', + '<<<[TOOL_REQUEST]>>> tool_name:「始」Demo「末」 <<<[END_TOOL_REQUEST]>>>', + '```js\nconst marker = "not a tool result";\n```' + ].join('\n'); + const result = pipeline.process(input, { mode: PIPELINE_MODES.FULL_RENDER }); + assert.equal(result.state.thoughtChainMap.size, 1); + assert.equal(result.state.toolResultMap.size, 1); + assert.equal(result.state.toolRequestMap.size, 1); + assert.equal(result.state.codeBlockMap.size, 1); + assert.deepEqual(result.meta.stepsApplied.slice(0, 5), [ + 'strip-persona-backfill-tail', + 'normalize-emoticon-urls', + 'protect-thought-chains', + 'protect-tool-results', + 'protect-tool-requests' + ]); + assert.match(result.state.toolRequestMap.values().next().value, /Demo/); +}); + +test('stream-fast protocol path is intentionally lightweight and does not create protection maps', () => { + const pipeline = createContentPipeline({ + getToolResultRegex: () => /\[RESULT:[\s\S]*?\]/g, + getCodeFenceRegex: () => /```[\s\S]*?```/g + }); + const result = pipeline.process('[RESULT: partial]', { mode: PIPELINE_MODES.STREAM_FAST }); + assert.equal(result.state.toolResultMap, null); + assert.equal(result.state.codeBlockMap, null); + assert.deepEqual(result.meta.stepsApplied, [ + 'strip-persona-backfill-tail', + 'normalize-emoticon-urls', + 'deindent-misinterpreted-code-blocks', + 'apply-common-content-processors', + 'normalize-adjacent-bold-boundaries' + ]); +}); diff --git a/tests/content-processor-owner.test.mjs b/tests/content-processor-owner.test.mjs new file mode 100644 index 000000000..0e7d6db3a --- /dev/null +++ b/tests/content-processor-owner.test.mjs @@ -0,0 +1,28 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createContentProcessor } from '../modules/renderer/contentProcessor.js'; + +test('interactive message buttons retain their Surface command and teardown owner', async () => { + const dom = new JSDOM('
      '); + const rootA = dom.window.document.getElementById('a'); + const rootB = dom.window.document.getElementById('b'); + const callsA = []; + const callsB = []; + const processorA = createContentProcessor(); + const processorB = createContentProcessor(); + processorA.initializeContentProcessor({ chatMessagesDiv: rootA, messageCommands: { handleSendMessage: text => callsA.push(text) }, uiHelper: {} }); + processorB.initializeContentProcessor({ chatMessagesDiv: rootB, messageCommands: { handleSendMessage: text => callsB.push(text) }, uiHelper: {} }); + processorA.processInteractiveButtons(rootA); + processorB.processInteractiveButtons(rootB); + + rootA.querySelector('button').dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + processorA.dispose(); + rootB.querySelector('button').dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + await new Promise(resolve => setTimeout(resolve, 25)); + + assert.deepEqual(callsA, [], 'disposed Surface retained a scheduled button send'); + assert.deepEqual(callsB, ['[[点击按钮:B]]']); + assert.equal(rootA.querySelector('button').dataset.vcpInteractive, undefined); + assert.equal(rootB.querySelector('button').dataset.vcpInteractive, 'true'); +}); diff --git a/tests/content-runtime.test.mjs b/tests/content-runtime.test.mjs new file mode 100644 index 000000000..00c8d96d6 --- /dev/null +++ b/tests/content-runtime.test.mjs @@ -0,0 +1,54 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createContentRuntime } from '../modules/chat/contentRuntime.js'; +import { createMermaidPlaceholderTransform, decodeHtmlEntities } from '../modules/chat/contentTransforms.js'; + +test('ContentRuntime delegates full/stream modes and extracts protocol chunks', () => { + const calls = []; + const runtime = createContentRuntime({ pipeline: { process: (text, options) => { calls.push(options.mode); return { text }; } } }); + assert.equal(runtime.processFull('a').text, 'a'); + assert.equal(runtime.processStream('b').text, 'b'); + assert.deepEqual(calls, ['full-render', 'stream-fast']); + assert.equal(runtime.extractChunkText({ choices: [{ delta: { content: 'x' } }] }), 'x'); + assert.equal(runtime.extractChunkText({ error: 'json_parse_error', raw: 'bad' }), ''); +}); + +test('ContentRuntime stream assembler has explicit append/reset state', () => { + const runtime = createContentRuntime({ pipeline: { process: () => ({ text: '' }) } }); + const assembler = runtime.createStreamAssembler('a'); + assert.equal(assembler.append('b'), 'ab'); + assert.equal(assembler.text, 'ab'); + assert.equal(assembler.reset(), ''); +}); + +test('ContentRuntime creates a frozen render model without DOM or Electron access', () => { + const runtime = createContentRuntime({ pipeline: { + process(text, options) { return { text: `${options.mode}:${text}`, state: Object.freeze({}) }; } + } }); + const model = runtime.createRenderModel({ content: 'hello', role: 'user', id: 'm1' }); + assert.equal(model.message.id, 'm1'); + assert.equal(model.text, 'full-render:hello'); + assert.equal(Object.isFrozen(model), true); + assert.equal(Object.isFrozen(model.message), true); +}); + +test('ContentRuntime normalizes attachment protocol data before any DOM consumer sees it', () => { + const runtime = createContentRuntime({ pipeline: { process: (text) => ({ text, state: {} }) } }); + const message = runtime.normalizeMessage({ content: 'x', attachments: [ + { type: 'image/png', src: 'file:///x.png', name: 'x.png' }, + null, + { src: 42 } + ] }); + assert.equal(message.attachments.length, 2); + assert.equal(message.attachments[1].type, 'application/octet-stream'); + assert.equal(message.attachments[1].name, '未命名附件'); + assert.equal(Object.isFrozen(message.attachments), true); +}); + +test('content transforms keep Mermaid conversion DOM-free and decode entities deterministically', () => { + assert.equal(decodeHtmlEntities('<div>AA</div>'), '
      AA
      '); + const transform = createMermaidPlaceholderTransform(); + const output = transform('```mermaid\ngraph TD\nA-->B\n```'); + assert.match(output, /mermaid-placeholder/); + assert.match(output, /data-mermaid-code=/); +}); diff --git a/tests/desktop-push-consumer.test.mjs b/tests/desktop-push-consumer.test.mjs new file mode 100644 index 000000000..f4d711497 --- /dev/null +++ b/tests/desktop-push-consumer.test.mjs @@ -0,0 +1,163 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createDesktopPushConsumer } from '../modules/renderer/desktopPushConsumer.js'; + +function createHarness() { + const intervals = new Map(); + const cleared = []; + let nextId = 1; + let statusListener = null; + let unsubscribeCalls = 0; + const pushes = []; + const scheduler = { + setInterval(callback, delay) { + const id = nextId++; + intervals.set(id, { callback, delay }); + return id; + }, + clearInterval(id) { + cleared.push(id); + intervals.delete(id); + }, + }; + const electronAPI = { + onDesktopStatus(listener) { + statusListener = listener; + return () => { unsubscribeCalls += 1; statusListener = null; }; + }, + desktopPush(payload) { pushes.push(payload); }, + }; + return { + scheduler, + electronAPI, + intervals, + cleared, + pushes, + emitStatus(value) { statusListener?.({ connected: value }); }, + captureStatusListener() { return statusListener; }, + get unsubscribeCalls() { return unsubscribeCalls; }, + }; +} + +test('desktop push consumer owns subscription, message timer, and terminal cleanup', () => { + const harness = createHarness(); + const consumer = createDesktopPushConsumer({ + electronAPI: harness.electronAPI, + scheduler: harness.scheduler, + createWidgetId: () => 'widget-1', + logger: { log() {}, warn() {} }, + }); + consumer.start(); + harness.emitStatus(true); + + const output = consumer.processToken( + 'message-1', + 'visible<<<[DESKTOP_PUSH]>>>
      desktop content<<<[DESKTOP_PUSH_END]>>>tail', + ); + + assert.equal(output, 'visibletail'); + assert.deepEqual(harness.pushes.map(push => push.action), ['create', 'append', 'finalize']); + assert.equal(harness.intervals.size, 0, 'closed block must release its throttle timer'); + assert.equal(consumer.getStateCount(), 1, 'message lease remains until stream terminal cleanup'); + + consumer.cleanupMessage('message-1'); + assert.equal(consumer.getStateCount(), 0); + consumer.dispose(); + assert.equal(harness.unsubscribeCalls, 1); +}); + +test('starting an owned consumer repeatedly does not duplicate the status subscription', () => { + const harness = createHarness(); + const consumer = createDesktopPushConsumer({ + electronAPI: harness.electronAPI, + scheduler: harness.scheduler, + logger: { log() {}, warn() {} }, + }); + + consumer.start(); + const firstListener = harness.captureStatusListener(); + consumer.start(); + + assert.equal(harness.captureStatusListener(), firstListener); + consumer.dispose(); + assert.equal(harness.unsubscribeCalls, 1); +}); + +test('dispose revokes late status, timer, and token side effects', () => { + const harness = createHarness(); + let clock = 1; + const consumer = createDesktopPushConsumer({ + electronAPI: harness.electronAPI, + scheduler: harness.scheduler, + now: () => clock, + createWidgetId: () => 'widget-2', + logger: { log() {}, warn() {} }, + }); + consumer.start(); + harness.emitStatus(true); + consumer.processToken('message-2', '<<<[DESKTOP_PUSH]>>>
      pending'); + assert.equal(harness.intervals.size, 1); + const lateStatus = harness.captureStatusListener(); + const lateTimer = [...harness.intervals.values()][0].callback; + const pushesBeforeDispose = harness.pushes.length; + + consumer.dispose(); + + assert.equal(harness.unsubscribeCalls, 1); + assert.equal(harness.intervals.size, 0); + assert.equal(consumer.getStateCount(), 0); + lateStatus({ connected: true }); + clock += 200_000; + lateTimer(); + const lateToken = '<<<[DESKTOP_PUSH_END]>>>'; + assert.equal(consumer.processToken('message-2', lateToken), lateToken); + assert.equal(harness.pushes.length, pushesBeforeDispose, 'late callbacks and tokens must lose Desktop IPC authority'); + + consumer.dispose(); + assert.equal(harness.unsubscribeCalls, 1, 'dispose must be idempotent'); +}); + +test('message cleanup revokes an already queued throttle callback', () => { + const harness = createHarness(); + const consumer = createDesktopPushConsumer({ + electronAPI: harness.electronAPI, + scheduler: harness.scheduler, + createWidgetId: () => 'widget-3', + logger: { log() {}, warn() {} }, + }); + consumer.start(); + harness.emitStatus(true); + consumer.processToken('message-3', '<<<[DESKTOP_PUSH]>>>
      pending'); + const queuedTick = [...harness.intervals.values()][0].callback; + const pushesBeforeCleanup = harness.pushes.length; + + consumer.cleanupMessage('message-3'); + queuedTick(); + + assert.equal(harness.pushes.length, pushesBeforeCleanup); + assert.equal(consumer.getStateCount(), 0); + assert.equal(harness.intervals.size, 0); + consumer.dispose(); +}); + +test('a queued timer from a closed block cannot act on a later block for the same message', () => { + const harness = createHarness(); + const consumer = createDesktopPushConsumer({ + electronAPI: harness.electronAPI, + scheduler: harness.scheduler, + createWidgetId: () => `widget-${harness.pushes.length}`, + logger: { log() {}, warn() {} }, + }); + consumer.start(); + harness.emitStatus(true); + consumer.processToken('message-4', '<<<[DESKTOP_PUSH]>>>
      first'); + const staleTick = [...harness.intervals.values()][0].callback; + consumer.processToken('message-4', '<<<[DESKTOP_PUSH_END]>>>'); + consumer.processToken('message-4', '<<<[DESKTOP_PUSH]>>>
      second'); + const pushesBeforeStaleTick = harness.pushes.length; + + staleTick(); + + assert.equal(harness.pushes.length, pushesBeforeStaleTick); + consumer.dispose(); +}); diff --git a/tests/dom-listener-owner.test.mjs b/tests/dom-listener-owner.test.mjs new file mode 100644 index 000000000..79f7f49be --- /dev/null +++ b/tests/dom-listener-owner.test.mjs @@ -0,0 +1,10 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createDomListenerOwner } from '../modules/renderer/domListenerOwner.js'; + +test('DOM listener owner removes registrations and ignores late adds', () => { + const added = []; const removed = []; const target = { addEventListener: (...args) => added.push(args), removeEventListener: (...args) => removed.push(args) }; + const owner = createDomListenerOwner(); const handler = () => {}; + assert.equal(owner.add(target, 'click', handler), true); owner.dispose(); owner.dispose(); assert.equal(owner.add(target, 'click', handler), false); + assert.equal(added.length, 1); assert.equal(removed.length, 1); assert.equal(removed[0][1], handler); +}); diff --git a/tests/emoticon-fixer-owner.test.mjs b/tests/emoticon-fixer-owner.test.mjs new file mode 100644 index 000000000..db18ba301 --- /dev/null +++ b/tests/emoticon-fixer-owner.test.mjs @@ -0,0 +1,16 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createEmoticonUrlFixer } from '../modules/renderer/emoticonUrlFixer.js'; + +test('emoticon catalogs remain isolated between renderer owners', async () => { + const fixerA = createEmoticonUrlFixer(); + const fixerB = createEmoticonUrlFixer(); + await Promise.all([ + fixerA.initialize({ getEmoticonLibrary: async () => [{ filename: 'happy.png', url: 'file:///A/表情包/happy.png' }] }), + fixerB.initialize({ getEmoticonLibrary: async () => [{ filename: 'happy.png', url: 'file:///B/表情包/happy.png' }] }), + ]); + + const stale = 'file:///missing/表情包/happy.png'; + assert.equal(fixerA.fixEmoticonUrl(stale), 'file:///A/表情包/happy.png'); + assert.equal(fixerB.fixEmoticonUrl(stale), 'file:///B/表情包/happy.png'); +}); diff --git a/tests/enhanced-color-utils-lifecycle.test.mjs b/tests/enhanced-color-utils-lifecycle.test.mjs new file mode 100644 index 000000000..2c11dc7d8 --- /dev/null +++ b/tests/enhanced-color-utils-lifecycle.test.mjs @@ -0,0 +1,14 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +test('enhanced color utils does not install a process-lifetime cleanup interval', async () => { + const originalSetInterval = globalThis.setInterval; + let calls = 0; + globalThis.setInterval = () => { calls += 1; return 1; }; + try { + await import('../modules/renderer/enhancedColorUtils.js?lifecycle-test=1'); + assert.equal(calls, 0); + } finally { + globalThis.setInterval = originalSetInterval; + } +}); diff --git a/tests/forward-message-owner.test.mjs b/tests/forward-message-owner.test.mjs new file mode 100644 index 000000000..6eab77cd8 --- /dev/null +++ b/tests/forward-message-owner.test.mjs @@ -0,0 +1,23 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createForwardMessageOwner } from '../modules/renderer/forwardMessageOwner.js'; + +function doc() { + const nodes = new Map(); + const make = id => ({ id, value:'', disabled:false, innerHTML:'', dataset:{}, style:{}, children:[], append(...xs){ this.children.push(...xs); }, querySelector(){ return null; }, querySelectorAll(){ return []; } }); + for (const id of ['forwardTargetList','forwardTargetSearch','forwardAdditionalComment','confirmForwardBtn']) nodes.set(id, make(id)); + return { getElementById: id => nodes.get(id), createElement: tag => make(tag) }; +} + +test('forward owner resolves original content and sends attachments', async () => { + const d = doc(); const sent=[]; let opened=false; + const owner = createForwardMessageOwner({ documentRef:d, chatAPI:{ getAllItems:async()=>({success:true,items:[]}), getOriginalMessageContent:async()=>({success:true,content:'hello'}) }, chatManager:{ handleForwardMessage:async(...args)=>sent.push(args) }, uiHelperFunctions:{openModal:()=>{opened=true;},closeModal:()=>{},showToastNotification:()=>{}}, getConversation:()=>({item:{id:'a',type:'agent'},topicId:'t'}) }); + await owner.show({id:'m',role:'assistant',attachments:['x']}); + assert.equal(opened,true); + await owner.confirm(); + assert.equal(sent.length,0); +}); + +test('disposed forward owner ignores new modal requests', async () => { + let opened=0; const owner=createForwardMessageOwner({documentRef:doc(),uiHelperFunctions:{openModal:()=>opened++}}); await owner.dispose(); await owner.show({id:'m'}); assert.equal(opened,0); +}); diff --git a/tests/frontend-plugins.test.js b/tests/frontend-plugins.test.js index 9f23052bf..0e70e004e 100644 --- a/tests/frontend-plugins.test.js +++ b/tests/frontend-plugins.test.js @@ -379,7 +379,8 @@ test('自动 TTS 开关注入 Sovits 设置区且只朗读启动后完成的新 }); const { window } = dom; createRegistry(window); - window.currentSelectedItem = { id: 'agent-a', type: 'agent', name: 'Agent A' }; + const selectedItem = Object.freeze({ id: 'agent-a', type: 'agent', name: 'Agent A' }); + window.VCPMainChatState = Object.freeze({ snapshot: () => Object.freeze({ selectedItem, topicId: 'topic-a' }) }); const calls = []; window.chatAPI = { getAgentConfig: async () => ({ diff --git a/tests/global-settings-save.test.mjs b/tests/global-settings-save.test.mjs index 3676d9077..15197b179 100644 --- a/tests/global-settings-save.test.mjs +++ b/tests/global-settings-save.test.mjs @@ -49,13 +49,16 @@ test('global settings saves the server URL once with canonical presentation', as }; dom.window.normalizeChatPresentationMode = () => 'bubble'; - const currentSettings = {}; + let currentSettings = {}; const deps = { - refs: { globalSettings: { get: () => currentSettings } }, + refs: { globalSettings: { get: () => currentSettings, set: value => { currentSettings = value; } } }, getCroppedFile: () => null, setCroppedFile() {}, uiHelperFunctions: { showToastNotification() {}, closeModal() {} }, settingsManager: { completeVcpUrl: url => url }, + normalizeChatPresentationMode: () => 'bubble', + applyChatPresentationMode: async () => ({ success: true, mode: 'bubble' }), + applyChatBubbleLayoutSettings() {}, }; const form = dom.window.document.getElementById('globalSettingsForm'); dom.window.document.getElementById('vcpServerUrl').value = 'http://localhost:6005'; diff --git a/tests/image-handler-owner.test.mjs b/tests/image-handler-owner.test.mjs new file mode 100644 index 000000000..8b35535a0 --- /dev/null +++ b/tests/image-handler-owner.test.mjs @@ -0,0 +1,35 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createImageHandler } from '../modules/renderer/imageHandler.js'; + +test('image handlers retain only their owning Surface transport', () => { + const dom = new JSDOM('
      ', { url: 'https://example.test/' }); + const callsA = []; + const callsB = []; + const rootA = dom.window.document.getElementById('a'); + const rootB = dom.window.document.getElementById('b'); + const handlerA = createImageHandler({ fixUrl: value => value }); + const handlerB = createImageHandler({ fixUrl: value => value }); + handlerA.initialize({ chatMessagesDiv: rootA, electronAPI: { openImageViewer: value => callsA.push(value), showImageContextMenu() {} } }); + handlerB.initialize({ chatMessagesDiv: rootB, electronAPI: { openImageViewer: value => callsB.push(value), showImageContextMenu() {} } }); + + handlerA.setContentAndProcessImages(rootA, 'A', 'a'); + handlerB.setContentAndProcessImages(rootB, 'B', 'b'); + rootA.querySelector('img').dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + rootB.querySelector('img').dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + + assert.equal(callsA.length, 1); + assert.equal(callsA[0].title, 'A'); + assert.equal(callsA[0].theme, 'light'); + assert.equal(callsB.length, 1); + assert.equal(callsB[0].title, 'B'); + const retiredImageA = rootA.querySelector('img'); + handlerA.dispose(); + retiredImageA.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + assert.equal(callsA.length, 1, 'disposed image owner retained click authority'); + rootB.querySelector('img').dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + assert.equal(callsB.length, 2, 'disposing one image owner affected another Surface'); + assert.throws(() => handlerA.setContentAndProcessImages(rootA, '', 'a'), /not initialized/); + assert.doesNotThrow(() => handlerB.setContentAndProcessImages(rootB, '', 'b')); +}); diff --git a/tests/input-enhancer-owner.test.mjs b/tests/input-enhancer-owner.test.mjs new file mode 100644 index 000000000..974336bad --- /dev/null +++ b/tests/input-enhancer-owner.test.mjs @@ -0,0 +1,81 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createDomListenerOwner } from '../modules/renderer/domListenerOwner.js'; + +const deferred = () => { + let resolve; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +}; +let inputEnhancer; + +test('input enhancer registers DOM and preload resources with the renderer owner', async () => { + globalThis.window = { electronPath: { basename: async value => value.split('\\').pop() } }; + globalThis.alert = () => {}; + await import('../modules/inputEnhancer.js?owner-test=2'); + inputEnhancer = window.inputEnhancer; + const owner = createDomListenerOwner(); + const listeners = new Map(); + const target = { + classList: { add() {}, remove() {}, contains() { return false; } }, + addEventListener(type, handler) { listeners.set(type, handler); }, + removeEventListener(type, handler) { if (listeners.get(type) === handler) listeners.delete(type); }, + }; + let sharedDisposed = 0; + const api = { + onAddFileToInput: () => () => { sharedDisposed += 1; }, + handleFileDrop: async () => [], + searchNotes: async () => [], + }; + window.inputEnhancer.initializeInputEnhancer({ + messageInput: target, + dropTargetElement: target, + electronAPI: api, + attachedFiles: { get: () => [], set() {}, append() {} }, + updateAttachmentPreview() {}, + getCurrentAgentId: () => 'agent', + getCurrentTopicId: () => 'topic', + listenerOwner: owner, + }); + assert.equal(listeners.size >= 6, true); + owner.dispose(); + assert.equal(listeners.size, 0); + assert.equal(sharedDisposed, 1); +}); + +test('input enhancer waits for pending preload work and suppresses its late attachment projection', async () => { + window.electronPath = { basename: async value => value.split('\\').pop() }; + globalThis.alert = () => {}; + const result = deferred(); + let consumeSharedFile; + let appended = 0; + let previews = 0; + const target = { + classList: { add() {}, remove() {}, contains() { return false; } }, + addEventListener() {}, removeEventListener() {}, + }; + inputEnhancer.initializeInputEnhancer({ + messageInput: target, + dropTargetElement: target, + electronAPI: { + onAddFileToInput: callback => { consumeSharedFile = callback; return () => {}; }, + handleFileDrop: () => result.promise, + searchNotes: async () => [], + }, + attachedFiles: { get: () => [], set() {}, append() { appended += 1; } }, + updateAttachmentPreview() { previews += 1; }, + getCurrentAgentId: () => 'agent', + getCurrentTopicId: () => 'topic', + }); + + const pending = consumeSharedFile('C:\\tmp\\late.txt'); + await new Promise(resolve => setImmediate(resolve)); + let disposed = false; + const disposing = inputEnhancer.dispose().then(() => { disposed = true; }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(disposed, false); + result.resolve([{ success: true, attachment: { name: 'late.txt', internalPath: '/tmp/late.txt' } }]); + await Promise.all([pending, disposing]); + assert.equal(appended, 0); + assert.equal(previews, 0); +}); diff --git a/tests/launchpad-controller.test.js b/tests/launchpad-controller.test.js index c5278f163..1a18b36c2 100644 --- a/tests/launchpad-controller.test.js +++ b/tests/launchpad-controller.test.js @@ -36,3 +36,25 @@ test('launchpad renders shared catalogs and preserves embedded/window actions', assert.equal(dom.window.document.activeElement, rerendered[1]); controller.dispose(); }); + +test('launchpad hides non-discoverable internal test apps while retaining other internal apps', () => { + const dom = new JSDOM('
      '); + const opened = []; + const controller = new LaunchpadController({ + document: dom.window.document, + getExternalApps: () => [], + getInternalApps: () => [ + { id: 'standalone-chat-history', title: '聊天历史', discoverable: false }, + { id: 'standalone-chat-compose', title: '独立聊天', discoverable: false }, + { id: 'showcase', title: '组件库', icon: 'widgets' }, + ], + getIcon: () => '', + openInternal: id => opened.push(id), + }); + controller.mount(); + assert.deepEqual([...dom.window.document.querySelectorAll('.next-ui-internal-app-item')] + .map(button => button.textContent), ['widgets组件库']); + assert.deepEqual(opened, []); + controller.dispose(); + dom.window.close(); +}); diff --git a/tests/lifecycle-inspector.test.js b/tests/lifecycle-inspector.test.js index 0e8092435..59e056e0e 100644 --- a/tests/lifecycle-inspector.test.js +++ b/tests/lifecycle-inspector.test.js @@ -18,12 +18,20 @@ test('lifecycle inspector reports ownership metadata without payload content', a chatTasks: [{ requestId: 'message-1', operation: 'chat:stream', state: 'running', ageMs: 1 }], }) }; window.eval(fs.readFileSync('modules/ui-system/lifecycle-inspector.js', 'utf8')); + const streamProvider = () => ({ activeMessageId: 'safe-stream', activeMessageIds: ['safe-stream'] }); + window.VCPLifecycleInspector.setStreamDiagnosticsProvider(streamProvider); + window.VCPLifecycleInspector.setStreamDiagnosticsProvider(streamProvider); + assert.throws( + () => window.VCPLifecycleInspector.setStreamDiagnosticsProvider(() => null), + /already registered/ + ); window.dispatchEvent(new window.CustomEvent('ui-mode-transition-state', { detail: { phase: 'settled', mode: 'next', generation: 3 } })); const renderer = window.VCPLifecycleInspector.snapshot(); const main = await window.VCPLifecycleInspector.snapshotMain(); assert.equal(renderer.mode, 'next'); assert.equal(renderer.transitions[0].generation, 3); assert.equal(renderer.performance[0].name, 'next.mount'); + assert.equal(renderer.streams.activeMessageId, 'safe-stream'); assert.equal(main.tasks[0].operation, 'embedded:create'); assert.equal(main.chatTasks[0].operation, 'chat:stream'); const serialized = JSON.stringify({ renderer, main }); diff --git a/tests/main-chat-attachment-owner.test.mjs b/tests/main-chat-attachment-owner.test.mjs new file mode 100644 index 000000000..9ee359579 --- /dev/null +++ b/tests/main-chat-attachment-owner.test.mjs @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatAttachmentOwner } from '../modules/renderer/mainChatAttachmentOwner.js'; + +test('attachment owner provides one stable ref and projects its current files', () => { + const previews = []; + const owner = createMainChatAttachmentOwner({ renderPreview: files => previews.push([...files]) }); + const ref = owner.ref; + const files = [{ name: 'one.txt' }]; + + assert.equal(owner.ref, ref); + const stored = ref.set(files); + assert.notEqual(stored, files); + assert.notEqual(owner.get(), files); + files.push({ name: 'outside.txt' }); + assert.deepEqual(owner.get(), [{ name: 'one.txt' }]); + const appended = ref.append({ name: 'two.txt' }); + assert.notEqual(appended, stored); + assert.deepEqual(owner.get(), [{ name: 'one.txt' }, { name: 'two.txt' }]); + assert.throws(() => owner.get().push({ name: 'bypass.txt' }), /extensible|read only/i); + assert.equal(owner.syncPreview(), true); + assert.deepEqual(previews, [[{ name: 'one.txt' }, { name: 'two.txt' }]]); + assert.deepEqual(owner.clear(), []); + assert.deepEqual(owner.get(), []); +}); + +test('attachment owner dispose is idempotent and rejects later mutation', () => { + const owner = createMainChatAttachmentOwner(); + owner.ref.set([{ name: 'one.txt' }]); + owner.dispose(); + owner.dispose(); + + assert.deepEqual(owner.get(), []); + assert.equal(owner.syncPreview(), false); + assert.throws(() => owner.ref.set([]), /disposed/); + assert.throws(() => owner.ref.append({ name: 'late.txt' }), /disposed/); + assert.throws(() => owner.clear(), /disposed/); +}); diff --git a/tests/main-chat-auxiliary-event-owner.test.mjs b/tests/main-chat-auxiliary-event-owner.test.mjs new file mode 100644 index 000000000..bb5d266d8 --- /dev/null +++ b/tests/main-chat-auxiliary-event-owner.test.mjs @@ -0,0 +1,54 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatAuxiliaryEventOwner } from '../modules/renderer/mainChatAuxiliaryEventOwner.js'; + +const channel = () => { + let listener = null; + let removed = 0; + return { + subscribe(callback) { listener = callback; return () => { listener = null; removed += 1; }; }, + emit(value) { listener?.(value); }, + get removed() { return removed; }, + }; +}; + +test('auxiliary event owner connects each producer to its explicit consumer', async () => { + const loom = channel(); + const status = channel(); + const message = channel(); + const topic = channel(); + const consumed = []; + const owner = createMainChatAuxiliaryEventOwner({ + subscriptions: { + loomShareText: callback => loom.subscribe(callback), + logStatus: callback => status.subscribe(callback), + logMessage: callback => message.subscribe(callback), + groupTopicUpdated: callback => topic.subscribe(callback), + }, + insertSharedText: value => consumed.push(['loom', value]), + consumeLogStatus: value => consumed.push(['status', value]), + consumeLogMessage: value => consumed.push(['message', value]), + consumeGroupTopicUpdate: value => consumed.push(['topic', value]), + }); + owner.mount(); + loom.emit('text'); status.emit('up'); message.emit('log'); topic.emit('topic'); + assert.deepEqual(consumed, [['loom', 'text'], ['status', 'up'], ['message', 'log'], ['topic', 'topic']]); + await owner.dispose(); + assert.deepEqual([loom.removed, status.removed, message.removed, topic.removed], [1, 1, 1, 1]); +}); + +test('auxiliary event owner is idempotent and revokes late delivery', async () => { + const loom = channel(); + const consumed = []; + const owner = createMainChatAuxiliaryEventOwner({ + subscriptions: { loomShareText: callback => loom.subscribe(callback) }, + insertSharedText: value => consumed.push(value), + }); + owner.mount(); + owner.mount(); + await owner.dispose(); + await owner.dispose(); + loom.emit('late'); + assert.deepEqual(consumed, []); + assert.equal(loom.removed, 1); +}); diff --git a/tests/main-chat-dom-bindings.test.mjs b/tests/main-chat-dom-bindings.test.mjs new file mode 100644 index 000000000..ec22f18d8 --- /dev/null +++ b/tests/main-chat-dom-bindings.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createMainChatDomBindings } from '../modules/renderer/mainChatDomBindings.js'; + +const requiredMarkup = ` +
        + +
        `; + +test('main chat DOM bindings resolve once from the owning document', () => { + const dom = new JSDOM(requiredMarkup); + const bindings = createMainChatDomBindings(dom.window.document); + assert.equal(bindings.chatMessagesDiv.ownerDocument, dom.window.document); + assert.equal(bindings.messageInput.id, 'messageInput'); + assert.equal(Object.isFrozen(bindings), true); + dom.window.close(); +}); + +test('main chat DOM bindings fail fast when the canonical contract is incomplete', () => { + const dom = new JSDOM('
        '); + assert.throws(() => createMainChatDomBindings(dom.window.document), /missing required node/); + assert.throws(() => createMainChatDomBindings(null), /owning document/); + dom.window.close(); +}); diff --git a/tests/main-chat-event-bridge.test.mjs b/tests/main-chat-event-bridge.test.mjs new file mode 100644 index 000000000..632c5dc98 --- /dev/null +++ b/tests/main-chat-event-bridge.test.mjs @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatEventBridge } from '../modules/renderer/mainChatEventBridge.js'; + +test('routes stream events to the owned consumer and disposes the preload subscription', async () => { + let listener; + let unsubscribed = 0; + const calls = []; + const bridge = createMainChatEventBridge({ + chatAPI: { onVCPStreamEvent(handler) { listener = handler; return () => { unsubscribed += 1; }; } }, + acceptStreamEvent(event) { calls.push(['stream', event]); return event.type === 'data'; }, + consumeNonStreamingEvent(event) { calls.push(['non-stream', event]); return event.type === 'full_response'; }, + onUnhandled(message, event) { calls.push(['unhandled', message, event]); }, + }); + assert.equal(await listener({ type: 'data', messageId: 'm1' }), true); + assert.equal(await listener({ type: 'full_response', messageId: 'm1' }), true); + assert.equal(await listener({ type: 'unknown', messageId: 'm1' }), false); + assert.equal(calls[0][0], 'stream'); + assert.equal(calls[1][0], 'stream'); + assert.equal(calls[2][0], 'non-stream'); + assert.equal(calls.at(-1)[0], 'unhandled'); + bridge.dispose(); + assert.equal(unsubscribed, 1); + assert.equal(await listener({ type: 'data', messageId: 'late' }), false); +}); + +test('rejects malformed events without touching the stream consumer', async () => { + let listener; + let accepted = 0; + const bridge = createMainChatEventBridge({ + chatAPI: { onVCPStreamEvent(handler) { listener = handler; return () => {}; } }, + acceptStreamEvent() { accepted += 1; return true; }, + onUnhandled() {}, + }); + assert.equal(await listener({ type: 'data' }), false); + assert.equal(accepted, 0); + bridge.dispose(); +}); diff --git a/tests/main-chat-flowlock-owner.test.mjs b/tests/main-chat-flowlock-owner.test.mjs new file mode 100644 index 000000000..c74061334 --- /dev/null +++ b/tests/main-chat-flowlock-owner.test.mjs @@ -0,0 +1,6 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatFlowlockOwner } from '../modules/renderer/mainChatFlowlockOwner.js'; +test('Flowlock owner handles commands and isolates response failures', async () => { let callback; const responses=[]; const input={value:'abc abc'}; const manager={start:async()=>{},stop:async()=>{},setCustomPrompt(){},getSession:()=>({status:'active',activeMessageId:'m'}),getActiveAgents:()=>['a']}; const owner=createMainChatFlowlockOwner({subscriptions:{command:cb=>{callback=cb;return()=>{};}},flowlockManager:manager,getSelectedItem:()=>({id:'a'}),messageInput:input,sendResponse:(d,r)=>{responses.push(r);if(r.command==='get')throw Error('sink');}}); owner.mount(); await callback({requestId:'1',command:'remove',target:'abc'}); await callback({requestId:'2',command:'status'}); await callback({requestId:'3',command:'unknown'}); assert.equal(input.value,' '); assert.equal(responses[1].status.isProcessing,true); assert.equal(responses[2].success,false); }); +test('Flowlock owner ignores late callbacks after dispose', async () => { let callback; let disposed=false; let count=0; const owner=createMainChatFlowlockOwner({subscriptions:{command:cb=>{callback=cb;return()=>{disposed=true;};}},flowlockManager:{},sendResponse:()=>{count++;}}); owner.mount(); await owner.dispose(); await callback({requestId:'late',command:'get'}); assert.equal(disposed,true); assert.equal(count,0); }); +test('Flowlock owner does not answer after an in-flight command loses its lifecycle', async () => { let callback; let release; const pending = new Promise(resolve => { release = resolve; }); const responses=[]; const owner=createMainChatFlowlockOwner({subscriptions:{command:cb=>{callback=cb;return()=>{};}},flowlockManager:{start:()=>pending},getSelectedItem:()=>({id:'agent'}),sendResponse:(_data,result)=>responses.push(result)}); owner.mount(); const handling=callback({requestId:'in-flight',command:'start',topicId:'topic'}); await new Promise(resolve=>setImmediate(resolve)); const disposing=owner.dispose(); release(); await Promise.all([handling,disposing]); assert.deepEqual(responses,[]); }); diff --git a/tests/main-chat-send-owner.test.mjs b/tests/main-chat-send-owner.test.mjs new file mode 100644 index 000000000..f2d103add --- /dev/null +++ b/tests/main-chat-send-owner.test.mjs @@ -0,0 +1,112 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatSendOwner } from '../modules/renderer/mainChatSendOwner.js'; + +const ref = value => ({ get: () => value, set: next => (value = next) }); +const button = () => { + const classes = new Set(); + return { + innerHTML: 'send', dataset: {}, title: '', attributes: {}, + classList: { toggle(name, active) { active ? classes.add(name) : classes.delete(name); }, contains: name => classes.has(name) }, + setAttribute(name, value) { this.attributes[name] = value; }, + }; +}; + +function harness({ history = [], item = { id: 'agent-a', type: 'agent' }, element = null } = {}) { + const sendButton = button(); + const historyRef = ref(history); + const calls = []; + const owner = createMainChatSendOwner({ + button: sendButton, + messagesRoot: { querySelector: () => element }, + historyRef, + selectedItemRef: ref(item), + topicIdRef: ref('topic-a'), + streamProjection: { + getActiveStreamingMessageId: () => null, + getActiveStreamingContext: () => null, + discardStreamingMessage: id => calls.push(['discard', id]), + }, + chatAPI: { interruptGroupRequest: async id => (calls.push(['group-interrupt', id]), { success: true }) }, + interruptHandler: { interrupt: async id => (calls.push(['agent-interrupt', id]), { success: true }) }, + getAdapter: () => ({ cancelStream: async (id, reason) => (calls.push(['cancel', id, reason]), null) }), + getChatManager: () => ({ handleSendMessage: async () => calls.push(['send']) }), + messageRenderer: { removeMessageById: (id, persist) => calls.push(['remove', id, persist]) }, + notify: (message, type) => calls.push(['notify', message, type]), + }); + return { owner, sendButton, historyRef, calls }; +} + +test('send owner preserves send projection and dispatch', async () => { + const { owner, sendButton, calls } = harness(); + assert.equal(owner.update(), true); + assert.equal(sendButton.dataset.mode, 'send'); + assert.equal(sendButton.innerHTML, 'send'); + assert.equal(sendButton.attributes['aria-label'], '发送消息'); + assert.equal(sendButton.attributes['aria-busy'], 'false'); + await owner.handleAction(); + assert.deepEqual(calls, [['send']]); +}); + +test('send owner projects interrupt mode and dispatches the matching agent request', async () => { + const message = { id: 'message-a', role: 'assistant', isThinking: true }; + const element = { isConnected: true, classList: { contains: () => false } }; + const { owner, sendButton, calls } = harness({ history: [message], element }); + owner.update(); + assert.equal(sendButton.dataset.mode, 'interrupt'); + assert.equal(sendButton.classList.contains('interrupt-mode'), true); + assert.equal(sendButton.title, '中止回复'); + assert.equal(sendButton.attributes['aria-label'], '中止回复'); + assert.equal(sendButton.attributes['aria-busy'], 'true'); + await owner.handleAction(); + assert.deepEqual(calls, [ + ['agent-interrupt', 'message-a'], + ['notify', '已发送中止信号。', 'success'], + ]); +}); + +test('send owner preserves failed-interrupt local cleanup', async () => { + const message = { id: 'message-g', role: 'assistant', isThinking: true, isGroupMessage: true }; + const element = { isConnected: true, classList: { contains: () => true } }; + const { historyRef, calls } = harness({ history: [message], item: { id: 'group-a', type: 'group' }, element }); + // Replace the successful group producer with the same observable failure path used by production. + const localOwner = createMainChatSendOwner({ + button: button(), messagesRoot: { querySelector: () => element }, historyRef, + selectedItemRef: ref({ id: 'group-a', type: 'group' }), topicIdRef: ref('topic-a'), + streamProjection: { discardStreamingMessage: id => calls.push(['discard', id]) }, + chatAPI: { interruptGroupRequest: async () => ({ success: false, error: 'offline' }) }, + interruptHandler: {}, getAdapter: () => ({ cancelStream: async (id, reason) => (calls.push(['cancel', id, reason]), null) }), + getChatManager: () => null, + messageRenderer: { removeMessageById: (id, persist) => calls.push(['remove', id, persist]) }, + notify: (messageText, type) => calls.push(['notify', messageText, type]), + }); + await localOwner.interrupt(); + assert.deepEqual(historyRef.get(), []); + assert.deepEqual(calls, [ + ['cancel', 'message-g', 'offline'], + ['discard', 'message-g'], + ['remove', 'message-g', false], + ['notify', '中止失败:offline,已在本地停止。', 'error'], + ]); +}); + +test('send owner suppresses late interrupt completion after dispose', async () => { + let resolveInterrupt; + const calls = []; + const message = { id: 'message-a', role: 'assistant', isThinking: true }; + const element = { isConnected: true, classList: { contains: () => false } }; + const owner = createMainChatSendOwner({ + button: button(), messagesRoot: { querySelector: () => element }, historyRef: ref([message]), + selectedItemRef: ref({ id: 'agent-a', type: 'agent' }), topicIdRef: ref('topic-a'), streamProjection: {}, chatAPI: {}, + interruptHandler: { interrupt: () => new Promise(resolve => { resolveInterrupt = resolve; }) }, + getAdapter: () => ({ cancelStream: () => calls.push('cancel') }), getChatManager: () => null, + messageRenderer: {}, notify: () => calls.push('notify'), + }); + const pending = owner.interrupt(); + owner.dispose(); + owner.dispose(); + resolveInterrupt({ success: true }); + assert.equal(await pending, false); + assert.deepEqual(calls, []); + assert.equal(await owner.handleAction(), false); +}); diff --git a/tests/main-chat-settings-owner.test.mjs b/tests/main-chat-settings-owner.test.mjs new file mode 100644 index 000000000..6597ab10d --- /dev/null +++ b/tests/main-chat-settings-owner.test.mjs @@ -0,0 +1,139 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createMainChatSettingsOwner } from '../modules/renderer/mainChatSettingsOwner.js'; +import { createDomListenerOwner } from '../modules/renderer/domListenerOwner.js'; +import { createMainChatThemeOwner } from '../modules/renderer/mainChatThemeOwner.js'; +import { createMainChatSettingsPresentationOwner } from '../modules/renderer/mainChatSettingsPresentationOwner.js'; + +test('settings owner publishes replacements and isolates snapshots', () => { + const published = []; const owner = createMainChatSettingsOwner({ initial: { theme: 'dark' }, publish: value => published.push(value) }); + owner.update('theme', 'light'); + const copy = owner.snapshot(); + assert.equal(owner.get().theme, 'light'); assert.equal(copy.theme, 'light'); assert.equal(Object.isFrozen(copy), true); assert.equal(published.length, 1); +}); + +test('settings owner read copies cannot mutate nested authority state', () => { + const owner = createMainChatSettingsOwner({ initial: { profile: { density: 'comfortable' }, rules: [{ enabled: true }] } }); + const borrowed = owner.get(); + borrowed.profile.density = 'compact'; + borrowed.rules[0].enabled = false; + assert.deepEqual(owner.get(), { profile: { density: 'comfortable' }, rules: [{ enabled: true }] }); + const snapshot = owner.snapshot(); + assert.equal(Object.isFrozen(snapshot.profile), true); + assert.equal(Object.isFrozen(snapshot.rules[0]), true); +}); + +test('disposed settings owner rejects further replacement', () => { + const owner = createMainChatSettingsOwner({ initial: { enabled: true } }); owner.dispose(); owner.replace({ enabled: false }); assert.equal(owner.get().enabled, true); +}); + +test('DOM listener owner removes registrations and ignores late adds', () => { + const added = []; const removed = []; const target = { addEventListener: (...args) => added.push(args), removeEventListener: (...args) => removed.push(args) }; + const owner = createDomListenerOwner(); const handler = () => {}; + assert.equal(owner.add(target, 'click', handler), true); owner.dispose(); assert.equal(owner.add(target, 'click', handler), false); + assert.equal(added.length, 1); assert.equal(removed.length, 1); assert.equal(removed[0][1], handler); +}); + +test('DOM listener owner captures direct EventTarget registrations during assembly', () => { + if (typeof EventTarget === 'undefined') return; + const owner = createDomListenerOwner(); const target = new EventTarget(); const handler = () => {}; + const release = owner.capture(); target.addEventListener('click', handler); release(); owner.dispose(); + assert.doesNotThrow(() => target.dispatchEvent(new Event('click'))); +}); + +test('DOM listener owner disposes observer-like resources', () => { + let disconnected = 0; const owner = createDomListenerOwner(); owner.own({ disconnect() { disconnected += 1; } }); owner.dispose(); owner.dispose(); assert.equal(disconnected, 1); +}); + +test('DOM listener owner cancels pending timeout on dispose', async () => { + let ran = false; const owner = createDomListenerOwner(); owner.timeout(() => { ran = true; }, 5); owner.dispose(); await new Promise(resolve => setTimeout(resolve, 15)); assert.equal(ran, false); +}); + +test('theme owner rolls back failed presentation persistence', async () => { + const settings = { chatPresentationMode: 'bubble' }; + const classList = { values: new Set(), remove(...values) { values.forEach(value => this.values.delete(value)); }, add(value) { this.values.add(value); } }; + const owner = createMainChatThemeOwner({ settingsOwner: { get: () => settings, update: (key, value) => { settings[key] = value; } }, documentRef: { body: { classList, removeAttribute() {} } }, saveSettings: async () => ({ success: false, error: 'no' }), scheduleFrame: callback => callback(), notify() {} }); + const result = await owner.applyPresentation('panel', { persist: true }); + assert.equal(result.success, false); assert.equal(settings.chatPresentationMode, 'bubble'); assert.equal(classList.values.has('chat-presentation-bubble'), true); +}); + +test('theme owner rejects work after dispose', () => { + const classList = { values: new Set(), remove() {}, add(value) { this.values.add(value); } }; const owner = createMainChatThemeOwner({ settingsOwner: { get: () => ({ chatPresentationMode: 'bubble' }), update() {} }, documentRef: { body: { classList, removeAttribute() {} } } }); owner.dispose(); owner.applyInitialTheme('dark'); assert.equal(classList.values.size, 0); +}); + +test('settings presentation owner reads the live authority and releases quick-switcher listeners', async () => { + const dom = new JSDOM(` + +
        + + +
        + `); + const settingsOwner = createMainChatSettingsOwner({ initial: { chatPresentationMode: 'bubble' } }); + const listenerOwner = createDomListenerOwner(); + const calls = []; + const owner = createMainChatSettingsPresentationOwner({ + documentRef: dom.window.document, + settingsOwner, + listenerOwner, + chatAPI: {}, + }); + owner.configureThemeOwner({ + normalizePresentation: mode => mode, + applyPresentation: async mode => { calls.push(mode); return { success: true, mode }; }, + applyInitialTheme() {}, + }); + + settingsOwner.update('chatPresentationMode', 'panel'); + owner.setupPresentationQuickSwitcher(); + const panel = dom.window.document.querySelector('[data-presentation-mode="panel"]'); + assert.equal(panel.getAttribute('aria-checked'), 'true'); + panel.click(); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(calls, ['panel']); + + listenerOwner.dispose(); + panel.click(); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(calls, ['panel']); + dom.window.close(); +}); + +test('settings presentation owner waits for pending startup settings and suppresses projection after dispose', async () => { + let resolveSettings; + const pendingSettings = new Promise(resolve => { resolveSettings = resolve; }); + const dom = new JSDOM(''); + const settingsOwner = createMainChatSettingsOwner({ initial: { chatPresentationMode: 'bubble' } }); + const listenerOwner = createDomListenerOwner(); + const releases = []; + const owner = createMainChatSettingsPresentationOwner({ + documentRef: dom.window.document, + settingsOwner, + listenerOwner, + chatAPI: {}, + }); + owner.configureThemeOwner({ + normalizePresentation: mode => mode || 'bubble', + applyPresentation: async mode => ({ success: true, mode }), + applyInitialTheme() {}, + }); + owner.configureStartup({ + loadSettings: () => pendingSettings, + startupThemeGate: { release: value => releases.push(value) }, + }); + + const loading = owner.loadAndApply(); + await new Promise(resolve => setImmediate(resolve)); + let disposed = false; + const disposing = owner.dispose().then(() => { disposed = true; }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(disposed, false); + resolveSettings({ chatPresentationMode: 'panel', sidebarWidth: 400 }); + await Promise.all([loading, disposing]); + assert.equal(settingsOwner.get().chatPresentationMode, 'bubble'); + assert.deepEqual(releases, []); + await owner.dispose(); + listenerOwner.dispose(); + dom.window.close(); +}); diff --git a/tests/main-chat-state-authority.test.mjs b/tests/main-chat-state-authority.test.mjs new file mode 100644 index 000000000..0e30638ff --- /dev/null +++ b/tests/main-chat-state-authority.test.mjs @@ -0,0 +1,42 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMainChatStateAuthority } from '../modules/chat/mainChatStateAuthority.js'; + +test('main chat state exposes immutable snapshots through a read-only consumer', () => { + const authority = createMainChatStateAuthority(); + const source = { id: 'agent-a', type: 'agent', config: { model: 'x' } }; + authority.setSelectedItem(source); + authority.setTopicId('topic-a'); + authority.historyRef.set([{ id: 'message-a' }]); + source.id = 'mutated'; + + const snapshot = authority.consumer.snapshot(); + assert.equal(snapshot.selectedItem.id, 'agent-a'); + assert.equal(snapshot.topicId, 'topic-a'); + assert.equal(Object.isFrozen(snapshot), true); + assert.equal(Object.isFrozen(snapshot.selectedItem), true); + assert.equal(snapshot.history[0].id, 'message-a'); + assert.equal(Object.isFrozen(snapshot.history), true); + assert.equal(Object.isFrozen(snapshot.selectedItem.config), true); + assert.equal(Object.isFrozen(snapshot.history[0]), true); + assert.throws(() => { snapshot.selectedItem.config.model = 'mutated'; }, /read only|extensible/i); + assert.throws(() => { snapshot.history[0].id = 'mutated'; }, /read only|extensible/i); + assert.equal(authority.snapshot().selectedItem.config.model, 'x'); + assert.equal(authority.snapshot().history[0].id, 'message-a'); + assert.equal('setSelectedItem' in authority.consumer, false); + assert.equal('historyRef' in authority.consumer, false); +}); + +test('main chat state refs update immutable snapshots without exposing setters', () => { + const authority = createMainChatStateAuthority(); + const selected = authority.selectedItemRef.set({ id: 'agent-b', config: { model: 'y' } }); + authority.topicIdRef.set('topic-b'); + const snapshot = authority.snapshot(); + + assert.equal(selected.id, 'agent-b'); + assert.equal(Object.isFrozen(selected), true); + assert.equal(snapshot.selectedItem, selected); + assert.equal(snapshot.topicId, 'topic-b'); + assert.equal('selectedItemRef' in authority.consumer, false); + assert.equal('topicIdRef' in authority.consumer, false); +}); diff --git a/tests/main-chat-surface-adapter.test.mjs b/tests/main-chat-surface-adapter.test.mjs new file mode 100644 index 000000000..3e4a9d635 --- /dev/null +++ b/tests/main-chat-surface-adapter.test.mjs @@ -0,0 +1,109 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createMainChatSurfaceAdapter } from '../modules/renderer/mainChatSurfaceAdapter.js'; + +test('MainChatSurfaceAdapter owns renderer, stream routes and quiescent teardown', async () => { + const dom = new JSDOM('
        '); + const root = dom.window.document.getElementById('root'); + let initialized = null; + let rendererDisposed = false; + const renderer = { + initializeMessageRenderer(value) { initialized = value; }, + renderHistory() {}, renderMessage() {}, + }; + const adapter = createMainChatSurfaceAdapter({ + root, + renderer, + repository: { getHistory: async () => [], saveHistory() {} }, + focusTarget: dom.window.document.querySelector('textarea'), + operations: { dispose: async () => {} }, + renderDependencies: {}, + streamServices: { + streamProjection: { + startStreamingMessage() {}, appendStreamChunk() {}, projectStreamTerminal() {}, + }, + historyPersistence: { commit() {} }, + messageRenderer: renderer, + getSelection: () => null, + getTopicId: () => null, + }, + disposeRenderer: async () => { rendererDisposed = true; }, + }); + assert.equal(initialized.chatDomRenderer, adapter.domRenderer); + const release = adapter.streamRoutes.register('m1', { kind: 'main-chat' }); + assert.equal(typeof release.retract, 'function'); + assert.equal(typeof release.cancel, 'function'); + release(); + await adapter.dispose(); + assert.equal(rendererDisposed, true); + assert.equal(root.hasAttribute('data-chat-surface'), false); + assert.throws(() => adapter.streamRoutes.register('late', {}), /disposed/); +}); + +test('MainChatSurfaceAdapter owns the window unload receipt and releases it on dispose', async () => { + const dom = new JSDOM('
        '); + const root = dom.window.document.getElementById('root'); + let disposed = 0; + const renderer = { initializeMessageRenderer() {}, renderHistory() {}, renderMessage() {} }; + const adapter = createMainChatSurfaceAdapter({ + root, + renderer, + repository: { getHistory: async () => [], saveHistory() {} }, + focusTarget: dom.window.document.querySelector('textarea'), + operations: { dispose: async () => {} }, + renderDependencies: {}, + streamServices: { + streamProjection: { startStreamingMessage() {}, appendStreamChunk() {}, projectStreamTerminal() {} }, + historyPersistence: { commit() {} }, + messageRenderer: renderer, + getSelection: () => null, + getTopicId: () => null, + }, + disposeRenderer: async () => { disposed += 1; }, + ownerWindow: dom.window, + }); + dom.window.dispatchEvent(new dom.window.Event('beforeunload')); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(disposed, 1); + await adapter.dispose(); + dom.window.close(); +}); + +test('main Surface send state follows the real stream terminal consumer', async () => { + const dom = new JSDOM('
        '); + const root = dom.window.document.getElementById('root'); + const renderer = { initializeMessageRenderer() {}, renderHistory() {}, renderMessage() {} }; + let settled; + const terminalSettled = new Promise(resolve => { settled = resolve; }); + const adapter = createMainChatSurfaceAdapter({ + root, + renderer, + repository: { getHistory: async () => [], saveHistory() {} }, + focusTarget: dom.window.document.querySelector('textarea'), + operations: { dispose: async () => {} }, + renderDependencies: {}, + streamServices: { + streamProjection: { + startStreamingMessage() {}, + appendStreamChunk() {}, + projectStreamTerminal: async (messageId, finishReason, context, payload) => ({ + messageId, finishReason, context, content: payload.fullResponse, history: [], + }), + }, + historyPersistence: { commit: projected => projected }, + messageRenderer: renderer, + getSelection: () => ({ id: 'agent-a' }), + getTopicId: () => 'topic-a', + notifySendStateChanged: settled, + }, + disposeRenderer: async () => {}, + }); + const context = { agentId: 'agent-a', topicId: 'topic-a' }; + assert.equal(adapter.acceptStreamEvent({ type: 'start', messageId: 'm1', streamOperationId: 'op1', context }), true); + assert.equal(adapter.acceptStreamEvent({ type: 'end', messageId: 'm1', streamOperationId: 'op1', context, fullResponse: 'done', finish_reason: 'completed' }), true); + const outcome = await terminalSettled; + assert.equal(outcome.event.type, 'completed'); + await adapter.dispose(); + dom.window.close(); +}); diff --git a/tests/main-chat-theme-owner.test.mjs b/tests/main-chat-theme-owner.test.mjs new file mode 100644 index 000000000..839e00592 --- /dev/null +++ b/tests/main-chat-theme-owner.test.mjs @@ -0,0 +1,4 @@ +import test from 'node:test'; import assert from 'node:assert/strict'; import { createMainChatThemeOwner } from '../modules/renderer/mainChatThemeOwner.js'; +const body=()=>({classList:{values:new Set(),remove(...xs){xs.forEach(x=>this.values.delete(x));},add(x){this.values.add(x);}},removeAttribute(){}}); +test('theme owner applies and rolls back failed presentation persistence', async()=>{ const settings={chatPresentationMode:'bubble'}; const b=body(); const owner=createMainChatThemeOwner({settingsOwner:{get:()=>settings,update:(k,v)=>settings[k]=v},documentRef:{body:b},saveSettings:async()=>({success:false,error:'no'}),scheduleFrame:fn=>fn(),notify:()=>{}}); const result=await owner.applyPresentation('panel',{persist:true}); assert.equal(result.success,false); assert.equal(settings.chatPresentationMode,'bubble'); assert.equal(b.classList.values.has('chat-presentation-bubble'),true); }); +test('theme owner ignores work after dispose',()=>{ const b=body(); const owner=createMainChatThemeOwner({settingsOwner:{get:()=>({chatPresentationMode:'bubble'}),update(){}},documentRef:{body:b}}); owner.dispose(); owner.applyInitialTheme('dark'); assert.equal(b.classList.values.size,0); }); diff --git a/tests/memory-chat-repository.test.mjs b/tests/memory-chat-repository.test.mjs new file mode 100644 index 000000000..9457ffc3a --- /dev/null +++ b/tests/memory-chat-repository.test.mjs @@ -0,0 +1,22 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createMemoryChatRepository } from '../modules/chat/memoryChatRepository.js'; + +test('memory chat repository owns cloned short-session history', async () => { + let history = [{ id: 'm1', content: 'before' }]; + const repository = createMemoryChatRepository({ + read: () => history, + write: next => { history = next; }, + }); + const loaded = await repository.getHistory('ignored', 'agent', 'temporary'); + loaded[0].content = 'mutated copy'; + assert.equal(history[0].content, 'before'); + const input = [{ id: 'm2', content: 'saved' }]; + await repository.saveHistory('ignored', 'agent', 'temporary', input); + input[0].content = 'late mutation'; + assert.equal(history[0].content, 'saved'); +}); + +test('memory chat repository fails loud without explicit state powers', () => { + assert.throws(() => createMemoryChatRepository(), /read and write/); +}); diff --git a/tests/message-edit-watcher-failure.test.js b/tests/message-edit-watcher-failure.test.js index 021b8c535..f89899f77 100644 --- a/tests/message-edit-watcher-failure.test.js +++ b/tests/message-edit-watcher-failure.test.js @@ -12,8 +12,8 @@ test('a watcher restart failure cannot roll back a durably saved message edit',
        old content
        `, { runScripts: 'outside-only', url: 'https://vcpchat.local/' }); const executableSource = source - .replace(/export\s*\{[\s\S]*?\};\s*$/, '') - .concat('\nwindow.__messageEditTestApi = { initializeContextMenu, toggleEditMode };\n'); + .replace('export function createMessageContextMenu()', 'function createMessageContextMenu()') + .concat('\nwindow.__messageEditTestApi = createMessageContextMenu();\n'); dom.window.eval(executableSource); const history = [{ id: 'message-1', role: 'assistant', content: 'old content' }]; @@ -25,15 +25,17 @@ test('a watcher restart failure cannot roll back a durably saved message edit', dom.window.__messageEditTestApi.initializeContextMenu({ electronAPI: { watcherBegin: async () => ({ success: true, token: 'edit-lease' }), - saveChatHistory: async (_itemId, _topicId, nextHistory) => { - durableWrites.push(JSON.parse(JSON.stringify(nextHistory))); - return { success: true }; - }, watcherStart: async () => { watcherRestarts += 1; throw new Error('controlled watcher restart failure'); }, }, + chatRepository: { + saveHistory: async (_itemId, _type, _topicId, nextHistory) => { + durableWrites.push(JSON.parse(JSON.stringify(nextHistory))); + return { success: true }; + }, + }, markedInstance: { parse: value => value }, uiHelper: { autoResizeTextarea() {}, @@ -44,6 +46,12 @@ test('a watcher restart failure cannot roll back a durably saved message edit', get: () => ({ id: 'agent-1', type: 'agent', config: { agentDataPath: '/fixture/agent-1' } }), }, currentTopicIdRef: { get: () => 'topic-1' }, + historyMutationAuthority: { + replace: async (_descriptor, nextHistory) => { + durableWrites.push(JSON.parse(JSON.stringify(nextHistory))); + return { result: { success: true }, history: nextHistory }; + }, + }, }, { updateMessageContent(_messageId, content) { renderedContent = content; }, }); diff --git a/tests/middle-click-owner.test.mjs b/tests/middle-click-owner.test.mjs new file mode 100644 index 000000000..271e82799 --- /dev/null +++ b/tests/middle-click-owner.test.mjs @@ -0,0 +1,58 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createMiddleClickHandler } from '../modules/renderer/middleClickHandler.js'; + +test('middle-click owner removes document capture listeners during dispose', () => { + const dom = new JSDOM('
        '); + const previous = { window: globalThis.window, document: globalThis.document }; + globalThis.window = dom.window; + globalThis.document = dom.window.document; + const documentListeners = new Map(); + const originalAdd = dom.window.document.addEventListener.bind(dom.window.document); + const originalRemove = dom.window.document.removeEventListener.bind(dom.window.document); + dom.window.document.addEventListener = (type, listener, options) => { + if (type === 'mouseup' || type === 'mouseleave' || type === 'mousemove') { + if (!documentListeners.has(type)) documentListeners.set(type, new Set()); + documentListeners.get(type).add(listener); + } + return originalAdd(type, listener, options); + }; + dom.window.document.removeEventListener = (type, listener, options) => { + documentListeners.get(type)?.delete(listener); + return originalRemove(type, listener, options); + }; + try { + const chatMessagesDiv = dom.window.document.getElementById('chat'); + const messageItem = dom.window.document.getElementById('message'); + const owner = createMiddleClickHandler(); + owner.initialize({ chatMessagesDiv, uiHelper: {} }, {}); + owner.startAdvancedMiddleClickTimer( + { clientX: 0, clientY: 0 }, + messageItem, + { id: 'message-1', content: 'done', isThinking: false }, + { middleClickAdvancedDelay: 10_000 }, + ); + assert.equal(documentListeners.get('mouseup')?.size, 1); + assert.equal(documentListeners.get('mouseleave')?.size, 1); + + owner.dispose(); + + assert.equal(documentListeners.get('mouseup')?.size, 0); + assert.equal(documentListeners.get('mouseleave')?.size, 0); + assert.equal(documentListeners.get('mousemove')?.size || 0, 0); + assert.deepEqual(owner.getMiddleClickState(), { + activeTimers: 0, + gridVisible: false, + currentSelection: '', + advancedModeActive: false, + isDeletingMessage: false, + freezeGridCancellation: false, + toastFunctionModified: false, + }); + } finally { + globalThis.window = previous.window; + globalThis.document = previous.document; + dom.window.close(); + } +}); diff --git a/tests/non-streaming-event-consumer.test.mjs b/tests/non-streaming-event-consumer.test.mjs new file mode 100644 index 000000000..a506f10cb --- /dev/null +++ b/tests/non-streaming-event-consumer.test.mjs @@ -0,0 +1,26 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createNonStreamingEventConsumer } from '../modules/renderer/nonStreamingEventConsumer.js'; + +test('non-streaming consumer projects only already-persisted current-surface events', async () => { + const calls = []; + const renderTarget = { + root: {}, + removeMessage(id, save) { calls.push(['remove', id, save]); }, + }; + const messageRenderer = { + renderFullMessageProjection(...args) { calls.push(['full', ...args]); return Promise.resolve(); }, + }; + const consumer = createNonStreamingEventConsumer({ + renderTarget, + messageRenderer, + viewAuthority: { isCurrent: context => context?.topicId === 'current' }, + }); + assert.equal(await consumer.consume({ type: 'full_response', messageId: 'm1', fullResponse: 'ok', context: { topicId: 'background' } }), true); + assert.equal(await consumer.consume({ type: 'full_response', messageId: 'm1', fullResponse: 'ok', context: { topicId: 'current' } }), true); + assert.equal(await consumer.consume({ type: 'remove_message', messageId: 'm1', context: { topicId: 'current' } }), true); + assert.deepEqual(calls.map(call => call[0]), ['full', 'remove']); + assert.equal(calls[1][2], false); + consumer.dispose(); + assert.equal(await consumer.consume({ type: 'remove_message', messageId: 'm2', context: { topicId: 'current' } }), false); +}); diff --git a/tests/notification-renderer-lifecycle.test.mjs b/tests/notification-renderer-lifecycle.test.mjs new file mode 100644 index 000000000..9bcf94199 --- /dev/null +++ b/tests/notification-renderer-lifecycle.test.mjs @@ -0,0 +1,29 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { JSDOM } from 'jsdom'; +import { createDomListenerOwner } from '../modules/renderer/domListenerOwner.js'; + +test('notification renderer cancels late toast projection after owner disposal', async () => { + const dom = new JSDOM(` +
        + +
          + `, { runScripts: 'outside-only' }); + dom.window.chatAPI = {}; + dom.window.eval(fs.readFileSync('modules/notificationRenderer.js', 'utf8')); + const owner = createDomListenerOwner(); + dom.window.notificationRenderer.configureCapabilities({ + filterManager: { checkMessageFilter: () => null }, + listenerOwner: owner, + }); + + const list = dom.window.document.getElementById('notificationsList'); + dom.window.notificationRenderer.renderVCPLogNotification('late toast', null, list, {}); + const toast = dom.window.document.querySelector('.floating-toast-notification'); + assert.ok(toast); + owner.dispose(); + await new Promise(resolve => setTimeout(resolve, 80)); + assert.equal(toast.classList.contains('visible'), false); + dom.window.close(); +}); diff --git a/tests/owned-preload-subscription.test.mjs b/tests/owned-preload-subscription.test.mjs new file mode 100644 index 000000000..ab7d93b47 --- /dev/null +++ b/tests/owned-preload-subscription.test.mjs @@ -0,0 +1,69 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createOwnedPreloadSubscription } from '../modules/renderer/ownedPreloadSubscription.js'; + +test('owned preload subscription forwards payloads and revokes late delivery', async () => { + let listener; + let released = 0; + const seen = []; + const owner = createOwnedPreloadSubscription({ + subscribe(handler) { listener = handler; return () => { released += 1; }; }, + consume(value) { seen.push(value); }, + }); + listener('first'); + await owner.dispose(); + listener('late'); + await owner.dispose(); + assert.deepEqual(seen, ['first']); + assert.equal(released, 1); +}); + +test('consumer errors are reported without destroying the subscription owner', async () => { + let listener; + const errors = []; + const owner = createOwnedPreloadSubscription({ + subscribe(handler) { listener = handler; return () => {}; }, + consume() { throw new Error('controlled'); }, + reportError(...args) { errors.push(args); }, + }); + listener('payload'); + assert.equal(errors.length, 1); + await owner.dispose(); +}); + +test('dispose waits for an in-flight async consumer and reports its rejection', async () => { + let listener; + let release; + const errors = []; + const pending = new Promise((resolve, reject) => { release = { resolve, reject }; }); + const owner = createOwnedPreloadSubscription({ + subscribe(handler) { listener = handler; return () => {}; }, + consume() { return pending; }, + reportError(...args) { errors.push(args); }, + }); + listener('payload'); + let disposed = false; + const disposal = owner.dispose().then(() => { disposed = true; }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(disposed, false); + release.reject(new Error('controlled async failure')); + await disposal; + assert.equal(errors.length, 1); +}); + +test('consumer receives an aborted lifecycle after disposal begins', async () => { + let listener; + let release; + const pending = new Promise(resolve => { release = resolve; }); + let lifecycle; + const owner = createOwnedPreloadSubscription({ + subscribe(handler) { listener = handler; return () => {}; }, + async consume(_payload, value) { lifecycle = value; await pending; }, + }); + listener('payload'); + const disposal = owner.dispose(); + assert.equal(lifecycle.isActive(), false); + assert.equal(lifecycle.signal.aborted, true); + release(); + await disposal; +}); diff --git a/tests/render-dependencies.test.mjs b/tests/render-dependencies.test.mjs new file mode 100644 index 000000000..838599700 --- /dev/null +++ b/tests/render-dependencies.test.mjs @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createRenderDependencies } from '../modules/renderer/renderDependencies.js'; + +const ref = value => ({ get: () => value, set: next => { value = next; } }); +const valid = () => { + const dom = new JSDOM('
          '); + return { + chatMessagesDiv: dom.window.document.getElementById('chat'), + currentChatHistoryRef: ref([]), + currentSelectedItemRef: ref({ id: 'a' }), + currentTopicIdRef: ref('t'), + globalSettingsRef: ref({}), + electronAPI: {}, + chatRepository: { saveHistory() {} }, + historyMutationAuthority: { replace() {} }, + transientStreamHistory: { prepare() {}, finalize() {} }, + markedInstance: { parse: value => value }, + uiHelper: { scrollToBottom() {} }, + summarizeTopicFromMessages() {}, + handleCreateBranch() {}, + messageCommands: { handleSendMessage() {} }, + __dom: dom, + }; +}; + +test('RenderDependencies creates one frozen explicit capability closure', () => { + const dependencies = createRenderDependencies(valid()); + assert.equal(Object.isFrozen(dependencies), true); + assert.equal(Object.isFrozen(dependencies.state), true); + assert.equal(dependencies.root, dependencies.chatMessagesDiv); + assert.equal(dependencies.transport, dependencies.electronAPI); + assert.equal(dependencies.state.history, dependencies.currentChatHistoryRef); + assert.equal(typeof dependencies.messageCommands.handleSendMessage, 'function'); + assert.equal('chatManager' in dependencies, false, + 'the renderer closure must expose narrow message commands rather than the full chat manager'); + inputDomCleanup(dependencies); +}); + +function inputDomCleanup(dependencies) { + dependencies.document?.defaultView?.close?.(); +} + +test('RenderDependencies fails fast for absent root, state, transport and parser powers', () => { + for (const key of ['chatMessagesDiv', 'currentChatHistoryRef', 'electronAPI', 'markedInstance', 'historyMutationAuthority', 'transientStreamHistory']) { + const input = valid(); + delete input[key]; + assert.throws(() => createRenderDependencies(input), /RenderDependencies requires/, key); + input.__dom?.window.close(); + } +}); diff --git a/tests/render-session-authority.test.mjs b/tests/render-session-authority.test.mjs new file mode 100644 index 000000000..038efaa59 --- /dev/null +++ b/tests/render-session-authority.test.mjs @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createRenderSessionAuthority } from '../modules/renderer/renderSessionAuthority.js'; + +test('render session generations are isolated per Surface root', () => { + const rootA = {}; + const rootB = {}; + const authority = createRenderSessionAuthority({ resolveDefaultRoot: () => rootA }); + const a1 = authority.capture(rootA); + const b1 = authority.capture(rootB); + + const a2 = authority.invalidate(rootA); + assert.equal(authority.isActive(a1), false); + assert.equal(authority.isActive(a2), true); + assert.equal(authority.isActive(b1), true); + + const b2 = authority.invalidate(rootB); + assert.equal(authority.isActive(a2), true); + assert.equal(authority.isActive(b1), false); + assert.equal(authority.isActive(b2), true); +}); + +test('default root and detached sessions remain explicit and immutable', () => { + const root = {}; + const authority = createRenderSessionAuthority({ resolveDefaultRoot: () => root }); + const initial = authority.capture(); + const next = authority.invalidate(); + assert.equal(authority.isActive(initial), false); + assert.equal(authority.isActive(next), true); + assert.equal(Object.isFrozen(next), true); + + const detached = createRenderSessionAuthority(); + const d1 = detached.capture(); + const d2 = detached.invalidate(); + assert.equal(detached.isActive(d1), false); + assert.equal(detached.isActive(d2), true); +}); diff --git a/tests/stream-consumer-registry.test.mjs b/tests/stream-consumer-registry.test.mjs new file mode 100644 index 000000000..73a8e3531 --- /dev/null +++ b/tests/stream-consumer-registry.test.mjs @@ -0,0 +1,60 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createStreamConsumerRegistry } from '../modules/chat/streamConsumerRegistry.js'; + +test('stream consumer routes are exact-owner scoped and absent after release', () => { + const registry = createStreamConsumerRegistry(); + const first = { kind: 'main-chat' }; + const releaseFirst = registry.register('m1', first); + assert.equal(registry.claim('m1').suppressed, false); + assert.throws( + () => registry.register('m1', { kind: 'independent-surface' }), + /already registered/, + ); + assert.equal(registry.claim('m1').suppressed, false, 'duplicate registration must not replace the current owner'); + releaseFirst(); + assert.equal(registry.claim('m1'), null); +}); + +test('stream consumer registry rejects registration after owner dispose', () => { + const registry = createStreamConsumerRegistry(); + registry.register('m2', { kind: 'main-chat' }); + const release = registry.register('captured', { append() {} }); + const captured = registry.claim('captured'); + registry.dispose(); + assert.equal(registry.claim('m2'), null); + assert.equal(captured.suppressed, true); + release(); + assert.throws(() => registry.register('late', {}), /disposed/); +}); + +test('owner retraction leaves a one-terminal tombstone for late stream events', () => { + const registry = createStreamConsumerRegistry(); + const calls = []; + const release = registry.register('late', { start: () => calls.push('start'), append: () => calls.push('append') }); + const captured = registry.claim('late'); + release.retract(); + const tombstone = registry.claim('late'); + assert.equal(tombstone.suppressed, true); + captured.start(); + captured.append(); + assert.deepEqual(calls, [], 'a route captured before retraction must lose projection authority'); + tombstone.release(); + assert.equal(registry.claim('late'), null); +}); + +test('a retracted route loses DOM authority but can still settle its operation promise', () => { + const registry = createStreamConsumerRegistry(); + const settlements = []; + const release = registry.register('owned', { + append: () => assert.fail('retracted projection must stay silent'), + settle: value => settlements.push(value), + }); + const route = registry.claim('owned'); + release.retract(); + route.append('late'); + route.settle({ type: 'discarded' }); + assert.deepEqual(settlements, [{ type: 'discarded' }]); + route.release(); + assert.equal(registry.claim('owned'), null); +}); diff --git a/tests/stream-coordinator.test.mjs b/tests/stream-coordinator.test.mjs new file mode 100644 index 000000000..10c18d1c3 --- /dev/null +++ b/tests/stream-coordinator.test.mjs @@ -0,0 +1,218 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createStreamCoordinator } from '../modules/chat/streamCoordinator.js'; + +const deferred = () => { + let resolve; + let reject; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +}; +const request = (id, conversationKey = 'agent:a/topic:t') => ({ sessionId: id, messageId: id, conversationKey }); +const waitUntil = async predicate => { + for (let attempt = 0; attempt < 50; attempt += 1) { + if (predicate()) return; + await Promise.resolve(); + } + assert.fail('condition did not settle through the operation promise chain'); +}; + +test('done waits for transport, persistence and owned cleanup before one terminal event', async () => { + const save = deferred(); + const cleanup = deferred(); + const events = []; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { + stream.own(() => cleanup.promise); + stream.pushChunk('a'); + stream.final({ render: 'ready' }); + return 'completed'; + }, + persist: () => save.promise, + }); + const handle = coordinator.start(request('s1'), { onEvent: event => events.push(event) }); + let settled = false; + handle.done.then(() => { settled = true; }); + await Promise.resolve(); + assert.deepEqual(events.map(event => event.type), ['started', 'chunk', 'final']); + save.resolve(); + await Promise.resolve(); + assert.equal(settled, false); + cleanup.resolve(); + const outcome = await handle.done; + assert.equal(outcome.kind, 'completed'); + assert.equal(outcome.persistence.status, 'saved'); + assert.deepEqual(events.map(event => event.type), ['started', 'chunk', 'final', 'completed']); +}); + +test('reader failures and disconnects normalize to one failed terminal', async () => { + for (const mode of ['throw', 'disconnect']) { + const events = []; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { + if (mode === 'throw') throw new Error('reader broke'); + stream.terminal('disconnect', { source: 'socket' }); + return 'completed'; + }, + }); + const outcome = await coordinator.start(request(`s-${mode}`), { onEvent: event => events.push(event) }).done; + assert.equal(outcome.kind, 'failed'); + assert.equal(events.filter(event => ['completed', 'failed', 'cancelled', 'discarded'].includes(event.type)).length, 1); + } +}); + +test('cancel aborts transport and rejects duplicate terminal ownership', async () => { + const running = deferred(); + let signal; + const events = []; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { + signal = stream.signal; + await running.promise; + stream.terminal('completed'); + }, + }); + const handle = coordinator.start(request('cancel'), { onEvent: event => events.push(event) }); + const done = handle.cancel('user-stop'); + assert.equal(signal.aborted, true); + running.resolve(); + const outcome = await done; + assert.equal(outcome.kind, 'cancelled'); + assert.deepEqual(events.map(event => event.type), ['started', 'cancelled']); +}); + +test('dispose detaches consumer before abort and waits for the blocked reader', async () => { + const running = deferred(); + const events = []; + let abortSeen = false; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { + stream.signal.addEventListener('abort', () => { abortSeen = true; }); + await running.promise; + stream.pushChunk('late'); + }, + }); + const handle = coordinator.start(request('dispose'), { onEvent: event => events.push(event) }); + const first = handle.dispose(); + const second = handle.dispose(); + assert.equal(first, second); + assert.equal(abortSeen, true); + let settled = false; + first.then(() => { settled = true; }); + await Promise.resolve(); + assert.equal(settled, false); + running.resolve(); + assert.equal((await first).kind, 'discarded'); + assert.deepEqual(events.map(event => event.type), ['started']); +}); + +test('external abort becomes cancelled and remains visible to the live surface', async () => { + const running = deferred(); + const external = new AbortController(); + const events = []; + const coordinator = createStreamCoordinator({ run: async () => running.promise }); + const handle = coordinator.start(request('external'), { signal: external.signal, onEvent: event => events.push(event) }); + external.abort(); + running.resolve(); + assert.equal((await handle.done).kind, 'cancelled'); + assert.deepEqual(events.map(event => event.type), ['started', 'cancelled']); +}); + +test('persistence failure produces a single failed outcome after completed transport', async () => { + const events = []; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { stream.pushChunk('answer'); return 'completed'; }, + persist: async () => { throw new Error('disk full'); }, + reportError: () => {}, + }); + const outcome = await coordinator.start(request('persist-fail'), { onEvent: event => events.push(event) }).done; + assert.equal(outcome.kind, 'failed'); + assert.equal(outcome.phase, 'persistence'); + assert.equal(outcome.transport.kind, 'completed'); + assert.equal(outcome.persistence.status, 'failed'); + assert.equal(events.filter(event => ['completed', 'failed', 'cancelled', 'discarded'].includes(event.type)).length, 1); +}); + +test('same message retry revokes the old lease but distinct messages in one topic coexist', async () => { + const runs = new Map(); + const saved = []; + const coordinator = createStreamCoordinator({ + run: async current => { + const gate = deferred(); + runs.set(current.sessionId + ':' + current.attempt, gate); + return gate.promise; + }, + persist: async value => { saved.push(value.request.sessionId + ':' + value.request.attempt); }, + }); + const old = coordinator.start({ ...request('same'), attempt: 1 }); + await Promise.resolve(); + const replacement = coordinator.start({ ...request('same'), attempt: 2 }); + const other = coordinator.start({ ...request('other'), attempt: 1 }); + await Promise.resolve(); + runs.get('same:1').resolve('completed'); + runs.get('other:1').resolve('completed'); + runs.get('same:2').resolve('completed'); + assert.equal((await old.done).kind, 'discarded'); + await Promise.all([replacement.done, other.done]); + assert.deepEqual(saved.sort(), ['other:1', 'same:2']); +}); + +test('persistence serializes per conversation but not across conversations', async () => { + const gates = new Map(); + const entered = []; + const coordinator = createStreamCoordinator({ + run: async () => 'completed', + persist: async value => { + entered.push(value.request.sessionId); + const gate = deferred(); + gates.set(value.request.sessionId, gate); + await gate.promise; + }, + }); + const first = coordinator.start(request('first', 'topic:one')); + const second = coordinator.start(request('second', 'topic:one')); + const independent = coordinator.start(request('third', 'topic:two')); + await waitUntil(() => entered.length === 2); + assert.deepEqual(entered.sort(), ['first', 'third']); + gates.get('third').resolve(); + gates.get('first').resolve(); + await waitUntil(() => entered.includes('second')); + assert.equal(entered.includes('second'), true); + gates.get('second').resolve(); + await Promise.all([first.done, second.done, independent.done]); +}); + +test('consumer failure does not block persistence, terminal completion or later work', async () => { + let persisted = false; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { stream.pushChunk('x'); return 'completed'; }, + persist: async () => { persisted = true; }, + reportError: () => {}, + }); + const outcome = await coordinator.start(request('consumer'), { onEvent: () => { throw new Error('bad consumer'); } }).done; + assert.equal(outcome.kind, 'completed'); + assert.equal(persisted, true); +}); + +test('coordinator dispose aborts every active run and waits for quiescence', async () => { + const gates = [deferred(), deferred()]; + let index = 0; + const signals = []; + const coordinator = createStreamCoordinator({ + run: async (_request, stream) => { + signals.push(stream.signal); + await gates[index++].promise; + }, + }); + coordinator.start(request('a')); + coordinator.start(request('b')); + const disposing = coordinator.dispose(); + assert.equal(signals.every(signal => signal.aborted), true); + let settled = false; + disposing.then(() => { settled = true; }); + await Promise.resolve(); + assert.equal(settled, false); + gates.forEach(gate => gate.resolve()); + await disposing; + assert.throws(() => coordinator.start(request('late')), /disposed/); +}); diff --git a/tests/stream-manager-terminal-cleanup.test.js b/tests/stream-manager-terminal-cleanup.test.js index a57de0cef..72b60c996 100644 --- a/tests/stream-manager-terminal-cleanup.test.js +++ b/tests/stream-manager-terminal-cleanup.test.js @@ -2,129 +2,310 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const fs = require('node:fs'); const { JSDOM } = require('jsdom'); -const source = fs.readFileSync('modules/renderer/streamManager.js', 'utf8'); +const originalWindow = global.window; +const bootstrapDom = new JSDOM(''); +global.window = bootstrapDom.window; +test.after(() => { + global.window = originalWindow; + bootstrapDom.window.close(); +}); + +const loadFactory = async () => { + const module = await import('../modules/renderer/streamManager.js'); + const historyModule = await import('../modules/chat/streamTransientHistory.js'); + createDependencies.historyFactory = historyModule.createStreamTransientHistory; + return module.createStreamProjection; +}; + +function createDependencies(dom, overrides = {}) { + const root = dom.window.document.getElementById('chat'); + const dependencies = { + chatRepository: { + getHistory: async () => [], + saveHistory: async () => ({ success: true }), + ...overrides.chatRepository, + }, + currentSelectedItemRef: { get: () => ({ id: 'visible-agent', type: 'agent' }) }, + currentTopicIdRef: { get: () => 'visible-topic' }, + currentChatHistoryRef: { get: () => [], set() {} }, + viewAuthority: { isCurrent: context => context?.agentId === 'visible-agent' && context?.topicId === 'visible-topic' }, + globalSettingsRef: { get: () => ({ enableSmoothStreaming: false }) }, + chatMessagesDiv: root, + renderMessage: () => null, + uiHelper: { scrollToBottom() {} }, + electronAPI: { onDesktopStatus: () => () => {} }, + ...overrides, + }; + dependencies.transientStreamHistory ||= createDependencies.historyFactory({ + repository: dependencies.chatRepository, + currentHistory: { + get: () => dependencies.currentChatHistoryRef.get(), + replace: history => dependencies.currentChatHistoryRef.set(history), + }, + }); + return dependencies; +} -function functionBody(name, nextExport) { - const start = source.indexOf(`export async function ${name}`); - const end = source.indexOf(nextExport, start); - assert.notEqual(start, -1, `${name} export is missing`); - assert.notEqual(end, -1, `${name} boundary is missing`); - return source.slice(start, end); +function normalizeDiagnostics(projection) { + const diagnostics = { ...projection.getDiagnostics() }; + diagnostics.activeMessageIds = Array.from(diagnostics.activeMessageIds); + return diagnostics; } -test('stream initialization discards owned state on background history and render failures', () => { - const body = functionBody('startStreamingMessage', 'export function appendStreamChunk'); - assert.match( - body, - /Could not load history for background message[\s\S]*?discardStreamingMessage\(messageId\);[\s\S]*?return null;/, - ); - assert.match( - body, - /Failed to render message item[\s\S]*?discardStreamingMessage\(messageId\);[\s\S]*?return null;/, - ); -}); +const emptyDiagnostics = { + activeMessageId: null, + activeMessageIds: [], + initialization: 0, + activeInitializations: 0, + contexts: 0, + pendingHistory: 0, + prebuffered: 0, + pendingFinalizations: 0, + chunkQueues: 0, + renderTimers: 0, + delayedCleanupTimers: 0, + desktopPushStates: 0, +}; -test('stream finalization discards owned state on every unrecoverable lookup failure', () => { - const body = functionBody('finalizeStreamedMessage', 'export function discardStreamingMessage'); - for (const marker of [ - 'No context available for message', - 'Could not load history for finalization', - 'not found in assistant history', - 'not found in history', - ]) { - const markerIndex = body.indexOf(marker); - assert.notEqual(markerIndex, -1, `missing terminal branch: ${marker}`); - const terminalWindow = body.slice(markerIndex, markerIndex + 420); - assert.match(terminalWindow, /discardStreamingMessage\(messageId\);[\s\S]*?return;/, `${marker} does not release stream state`); +test('two StreamProjection owners isolate identical message identities and teardown listeners', async () => { + const createStreamProjection = await loadFactory(); + const domA = new JSDOM('
          ', { url: 'https://a.vcpchat.local/' }); + const domB = new JSDOM('
          ', { url: 'https://b.vcpchat.local/' }); + const listenerCounts = new Map([[domA.window, { add: 0, remove: 0 }], [domB.window, { add: 0, remove: 0 }]]); + for (const target of [domA.window, domB.window]) { + const originalAdd = target.addEventListener.bind(target); + const originalRemove = target.removeEventListener.bind(target); + target.addEventListener = (type, listener, options) => { + if (type === 'beforeunload') listenerCounts.get(target).add += 1; + return originalAdd(type, listener, options); + }; + target.removeEventListener = (type, listener, options) => { + if (type === 'beforeunload') listenerCounts.get(target).remove += 1; + return originalRemove(type, listener, options); + }; } + + const projectionA = createStreamProjection(); + const projectionB = createStreamProjection(); + projectionA.attachStreamProjection(createDependencies(domA)); + projectionB.attachStreamProjection(createDependencies(domB)); + projectionA.appendStreamChunk('same-message', { content: 'A' }, { agentId: 'a', topicId: 'topic-a' }); + projectionB.appendStreamChunk('same-message', { content: 'B' }, { agentId: 'b', topicId: 'topic-b' }); + + assert.equal(projectionA.getDiagnostics().prebuffered, 1); + assert.equal(projectionB.getDiagnostics().prebuffered, 1); + await projectionA.dispose(); + assert.deepEqual(normalizeDiagnostics(projectionA), emptyDiagnostics); + assert.equal(projectionB.getDiagnostics().prebuffered, 1, 'disposing A must not clear B runtime'); + await projectionB.dispose(); + assert.deepEqual(listenerCounts.get(domA.window), { add: 1, remove: 1 }); + assert.deepEqual(listenerCounts.get(domB.window), { add: 1, remove: 1 }); + domA.window.close(); + domB.window.close(); }); -test('discardStreamingMessage releases every strong stream owner', () => { - const start = source.indexOf('export function discardStreamingMessage'); - const end = source.indexOf('export function cleanupTransientState', start); - const body = source.slice(start, end); - for (const owner of [ - 'streamingChunkQueues', - 'streamingTimers', - 'pendingDirectRenderMessages', - 'accumulatedStreamText', - 'streamSegmentStates', - 'messageDomCache', - 'preBufferedChunks', - 'messageInitializationStatus', - 'pendingFinalizationEvents', - 'pendingHistoryEntries', - 'messageContextMap', - 'viewContextCache', - ]) { - assert.match(body, new RegExp(`${owner}\\.delete\\(messageId\\)`), `${owner} is not released`); - } - assert.match(body, /cleanupDesktopPushState\(messageId\)/); - assert.match(body, /updateSendButtonState/); +test('disposed StreamProjection rejects initialization and ignores every late stream event', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + const projection = createStreamProjection(); + const dependencies = createDependencies(dom); + projection.attachStreamProjection(dependencies); + await projection.dispose(); + + assert.equal(projection.appendStreamChunk('late', { content: 'late' }, { agentId: 'a', topicId: 't' }), false); + assert.equal(await projection.startStreamingMessage({ id: 'late', agentId: 'a', topicId: 't' }), null); + assert.equal(await projection.projectStreamTerminal('late', 'stop', { agentId: 'a', topicId: 't' }), null); + assert.equal(projection.discardStreamingMessage('late'), false); + assert.deepEqual(normalizeDiagnostics(projection), emptyDiagnostics); + assert.throws(() => projection.attachStreamProjection(dependencies), /disposed/); + await projection.dispose(); + dom.window.close(); }); -test('a runtime background-history failure releases every stream owner', async () => { - const dom = new JSDOM('
          ', { - runScripts: 'outside-only', - url: 'https://vcpchat.local/', - }); - const executableSource = source - .replace(/^import .*;$/gm, '') - .replace(/\bexport\s+(?=(?:async\s+)?function\b)/g, ''); - dom.window.formatMessageTimestamp = () => 'now'; - dom.window.PIPELINE_MODES = { STREAM_FAST: 'stream-fast' }; - dom.window.createContentPipeline = () => ({ process: text => ({ text }) }); - dom.window.updateSendButtonState = () => {}; - dom.window.eval(` - const formatMessageTimestamp = window.formatMessageTimestamp; - const PIPELINE_MODES = window.PIPELINE_MODES; - const createContentPipeline = window.createContentPipeline; - ${executableSource} - `); +test('StreamProjection fails fast when transient history or view authority is missing', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          '); + const dependencies = createDependencies(dom); + delete dependencies.transientStreamHistory; + assert.throws(() => createStreamProjection().attachStreamProjection(dependencies), /explicit transient history capability/); + const withHistory = createDependencies(dom); + delete withHistory.viewAuthority; + assert.throws(() => createStreamProjection().attachStreamProjection(withHistory), /explicit view authority/); + dom.window.close(); +}); + +test('owned StreamProjection completes terminal DOM projection without a cross-root side channel', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); const history = []; - const selected = { id: 'visible-agent', type: 'agent' }; - const api = dom.window.streamManager; - api.initStreamManager({ - electronAPI: { - getChatHistory: async () => { throw new Error('controlled history failure'); }, + let scrollCount = 0; + const projection = createStreamProjection(); + projection.attachStreamProjection(createDependencies(dom, { + currentChatHistoryRef: { get: () => history, set: value => { history.splice(0, history.length, ...value); } }, + uiHelper: { scrollToBottom() { scrollCount += 1; } }, + prepareFinalTextForRender: (_messageId, text, role) => ({ text, role, depth: 0 }), + renderPostProcessedHtml: async (content, html) => { content.textContent = html; }, + })); + + const messageItem = dom.window.document.querySelector('.message-item'); + await projection.startStreamingMessage({ id: 'owned', agentId: 'visible-agent', topicId: 'visible-topic', content: '' }, messageItem); + const projected = await projection.projectStreamTerminal( + 'owned', + 'completed', + { agentId: 'visible-agent', topicId: 'visible-topic' }, + { fullResponse: 'fixture terminal' }, + ); + + assert.equal(projected?.content, 'fixture terminal'); + assert.equal(messageItem.classList.contains('streaming'), false); + assert.match(messageItem.querySelector('.md-content').textContent, /fixture terminal/); + assert.ok(scrollCount >= 2, 'start and terminal projection should scroll only their owned root capability'); + assert.deepEqual(normalizeDiagnostics(projection), emptyDiagnostics); + await projection.dispose(); + dom.window.close(); +}); + +test('background initialization failure releases every runtime owner', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + const previousWindow = global.window; + global.window = dom.window; + dom.window.updateSendButtonState = () => {}; + try { + const projection = createStreamProjection(); + projection.attachStreamProjection(createDependencies(dom, { + chatRepository: { getHistory: async () => { throw new Error('controlled history failure'); } }, + })); + projection.appendStreamChunk('background-message', { content: 'buffered-before-init' }, { + agentId: 'background-agent', + topicId: 'background-topic', + }); + await projection.startStreamingMessage({ + id: 'background-message', + agentId: 'background-agent', + topicId: 'background-topic', + content: '', + }); + + assert.deepEqual(normalizeDiagnostics(projection), emptyDiagnostics); + await projection.dispose(); + } finally { + global.window = previousWindow; + dom.window.close(); + } +}); + +test('StreamProjection honors an owned view authority and history capability after navigation', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + let currentView = 'visible-topic'; + let history = []; + let writes = 0; + const projection = createStreamProjection(); + projection.attachStreamProjection(createDependencies(dom, { + currentChatHistoryRef: { get: () => history, set: value => { writes += 1; history = [...value]; } }, + viewAuthority: { isCurrent: context => context?.topicId === currentView }, + renderMessage: async message => { + const node = dom.window.document.createElement('article'); + node.className = 'message-item'; + node.dataset.messageId = message.id; + node.innerHTML = '
          '; + dom.window.document.getElementById('chat').appendChild(node); + return node; }, - currentSelectedItemRef: { get: () => selected }, - currentTopicIdRef: { get: () => 'visible-topic' }, - currentChatHistoryRef: { get: () => history, set() {} }, - globalSettingsRef: { get: () => ({ enableSmoothStreaming: false }) }, - chatMessagesDiv: dom.window.document.getElementById('chat'), - renderMessage: () => null, - uiHelper: {}, - }); + })); - api.appendStreamChunk('background-message', { content: 'buffered-before-init' }, { - agentId: 'background-agent', - topicId: 'background-topic', - }); - await api.startStreamingMessage({ - id: 'background-message', - agentId: 'background-agent', - topicId: 'background-topic', - content: '', - }); + const item = { id: 'background', agentId: 'visible-agent', topicId: 'visible-topic', content: '' }; + await projection.startStreamingMessage(item); + currentView = 'other-topic'; + await projection.projectStreamTerminal('background', 'completed', { agentId: 'visible-agent', topicId: 'visible-topic' }, { fullResponse: 'late terminal' }); - assert.deepEqual({ ...api.getDiagnostics() }, { - activeMessageId: null, - initialization: 0, - activeInitializations: 0, - contexts: 0, - pendingHistory: 0, - prebuffered: 0, - pendingFinalizations: 0, - chunkQueues: 0, - renderTimers: 0, - delayedCleanupTimers: 0, - historySaveQueue: 0, - desktopPushStates: 0, + assert.equal(writes, 1, 'history authority should receive the initialization snapshot once'); + assert.deepEqual(history.find(message => message.id === 'background')?.content, ''); + await projection.dispose(); + dom.window.close(); +}); + +test('active stream waiting for tool result can reconcile after switching away and back', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + let currentTopic = 'topic-a'; + let history = []; + const projection = createStreamProjection(); + const dependencies = createDependencies(dom, { + currentChatHistoryRef: { get: () => history, set: value => { history = [...value]; } }, + currentTopicIdRef: { get: () => currentTopic }, + viewAuthority: { isCurrent: context => context?.topicId === currentTopic }, + renderMessage: async message => { + const node = dom.window.document.createElement('article'); + node.className = 'message-item'; + node.dataset.messageId = message.id; + node.innerHTML = '
          '; + dom.window.document.getElementById('chat').append(node); + return node; + }, + renderPostProcessedHtml: async (content, html) => { content.innerHTML = html; }, }); + projection.attachStreamProjection(dependencies); + await projection.startStreamingMessage({ id: 'tool-wait', agentId: 'visible-agent', topicId: 'topic-a', content: '' }); + projection.appendStreamChunk('tool-wait', { content: 'before tool ' }, { agentId: 'visible-agent', topicId: 'topic-a' }); + currentTopic = 'topic-b'; + dom.window.document.getElementById('chat').replaceChildren(); + history = []; + currentTopic = 'topic-a'; + + const snapshot = projection.snapshotConversation({ itemType: 'agent', itemId: 'visible-agent', topicId: 'topic-a' }); + assert.equal(snapshot.length, 1); + await projection.reconcileConversation({ itemType: 'agent', itemId: 'visible-agent', topicId: 'topic-a' }); + projection.appendStreamChunk('tool-wait', { content: 'after tool' }, { agentId: 'visible-agent', topicId: 'topic-a' }); + await new Promise(resolve => setTimeout(resolve, 80)); + + assert.ok(history.some(message => message.id === 'tool-wait'), 'pending assistant must be restored to current history'); + assert.match(dom.window.document.querySelector('[data-message-id="tool-wait"] .md-content')?.textContent || '', /before tool/); + assert.match(dom.window.document.querySelector('[data-message-id="tool-wait"] .md-content')?.textContent || '', /after tool/); + await projection.dispose(); + dom.window.close(); +}); + +test('StreamProjection rejects chunks and terminals from a different producer operation', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + const projection = createStreamProjection(); + projection.attachStreamProjection(createDependencies(dom, { + renderPostProcessedHtml: async (content, html) => { content.textContent = html; }, + })); + const item = dom.window.document.querySelector('.message-item'); + await projection.startStreamingMessage({ id: 'operation', agentId: 'visible-agent', topicId: 'visible-topic', content: '', streamOperationId: 'op-a' }, item); + assert.equal(projection.appendStreamChunk('operation', { content: 'late' }, { agentId: 'visible-agent', topicId: 'visible-topic' }, 'op-b'), false); + assert.equal(await projection.projectStreamTerminal('operation', 'completed', { agentId: 'visible-agent', topicId: 'visible-topic' }, { fullResponse: 'late', streamOperationId: 'op-b' }), null); + await projection.dispose(); + dom.window.close(); +}); + +test('StreamProjection retires prior runtime state before same-message retry', async () => { + const createStreamProjection = await loadFactory(); + const dom = new JSDOM('
          ', { url: 'https://vcpchat.local/' }); + const projection = createStreamProjection(); + projection.attachStreamProjection(createDependencies(dom, { + renderMessage: async message => { + const node = dom.window.document.createElement('article'); + node.className = 'message-item'; + node.dataset.messageId = message.id; + node.innerHTML = '
          '; + dom.window.document.getElementById('chat').append(node); + return node; + }, + })); + const first = { id: 'retry-message', agentId: 'visible-agent', topicId: 'visible-topic', content: '', streamOperationId: 'op-a' }; + const second = { ...first, streamOperationId: 'op-b' }; + await projection.startStreamingMessage(first); + projection.appendStreamChunk(first.id, { content: 'old' }, first, 'op-a'); + await projection.startStreamingMessage(second); + assert.equal(projection.getDiagnostics().activeMessageIds.length, 1); + assert.equal(projection.getDiagnostics().prebuffered, 0); + await projection.dispose(); dom.window.close(); }); diff --git a/tests/stream-session.test.mjs b/tests/stream-session.test.mjs new file mode 100644 index 000000000..b470a3574 --- /dev/null +++ b/tests/stream-session.test.mjs @@ -0,0 +1,92 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + createStreamSession, + createStreamStateReducer, + normalizeStreamTerminal, +} from '../modules/chat/streamSession.js'; + +test('stream session emits ordered immutable chunks and one terminal outcome', () => { + const session = createStreamSession({ sessionId: 's1', conversationKey: 'agent:a/topic:t', generation: 2 }); + const events = []; + session.subscribe(event => events.push(event)); + assert.equal(session.pushChunk('a'), true); + assert.equal(session.pushChunk({ text: 'b' }), true); + assert.equal(session.terminal('completed', { messageId: 'm1' }), true); + assert.equal(session.terminal('failed', { error: 'late' }), false); + assert.deepEqual(events.map(event => event.type), ['chunk', 'chunk', 'completed']); + assert.equal(session.snapshot.text, 'ab'); + assert.equal(Object.isFrozen(session.snapshot), true); +}); + +test('stale generation and late chunks cannot regain authority', () => { + const session = createStreamSession({ sessionId: 's2', conversationKey: 'agent:a/topic:t', generation: 7 }); + assert.equal(session.pushChunk('stale', 6), false); + assert.equal(session.terminal('cancelled', { source: 'user' }), true); + assert.equal(session.pushChunk('late'), false); + assert.equal(session.terminal('completed'), false); +}); + +test('dispose publishes one discarded terminal, then silences consumers and is idempotent', async () => { + const session = createStreamSession({ sessionId: 's3', conversationKey: 'agent:a/topic:t' }); + const events = []; + session.subscribe(event => events.push(event)); + const first = session.dispose('surface-closed'); + const second = session.dispose('ignored-late-reason'); + assert.equal(session.pushChunk('late'), false); + assert.equal(session.terminal('discarded'), false); + assert.deepEqual(events.map(event => event.type), ['discarded']); + assert.deepEqual(await first, { kind: 'discarded', reason: 'surface-closed' }); + assert.deepEqual(await second, { kind: 'discarded', reason: 'surface-closed' }); +}); + +test('subscriber failure does not block later stream consumers', () => { + const session = createStreamSession({ sessionId: 's4', conversationKey: 'agent:a/topic:t' }); + const seen = []; + session.subscribe(() => { throw new Error('controlled consumer failure'); }); + session.subscribe(event => seen.push(event.type)); + session.pushChunk('x'); + assert.deepEqual(seen, ['chunk']); +}); + +test('done resolves once from the accepted terminal even when a consumer throws', async () => { + const session = createStreamSession({ sessionId: 's6', conversationKey: 'agent:a/topic:t' }); + let resolveCount = 0; + session.done.then(() => { resolveCount += 1; }); + session.subscribe(event => { + if (event.type === 'completed') throw new Error('controlled terminal consumer failure'); + }); + assert.equal(session.terminal('completed', { messageId: 'm6' }), true); + assert.equal(session.terminal('failed', { error: 'late' }), false); + assert.deepEqual(await session.done, { kind: 'completed', messageId: 'm6' }); + await Promise.resolve(); + assert.equal(resolveCount, 1); +}); + +test('normalizes transport terminal aliases into the four protocol outcomes', () => { + const cases = new Map([ + ['end', 'completed'], ['done', 'completed'], ['success', 'completed'], ['stop', 'completed'], + ['abort', 'cancelled'], ['cancel', 'cancelled'], + ['disconnect', 'failed'], ['reader-error', 'failed'], ['error', 'failed'], + ['stale', 'discarded'], ['discard', 'discarded'], + ]); + for (const [input, expected] of cases) { + assert.equal(normalizeStreamTerminal(input).kind, expected, input); + } + assert.deepEqual( + normalizeStreamTerminal({ type: 'unknown', detail: 'kept' }, { kind: 'cancelled', source: 'fallback' }), + { kind: 'cancelled', source: 'fallback', type: 'unknown', detail: 'kept' }, + ); + assert.throws(() => normalizeStreamTerminal({ type: 'unknown' }), /unknown chat stream terminal/); +}); + +test('reducer mirrors events and ignores a late event after terminal', () => { + const reduce = createStreamStateReducer(); + const initial = Object.freeze({ sessionId: 's5', status: 'started', text: '', chunkCount: 0, terminal: null }); + const next = reduce(initial, { sessionId: 's5', type: 'chunk', text: 'hello' }); + const done = reduce(next, { sessionId: 's5', type: 'completed', terminal: { kind: 'completed' } }); + assert.equal(done.text, 'hello'); + assert.equal(done.chunkCount, 1); + assert.equal(done.status, 'completed'); + assert.equal(reduce(done, { sessionId: 's5', type: 'chunk', text: 'late' }), done); +}); diff --git a/tests/stream-transient-history.test.mjs b/tests/stream-transient-history.test.mjs new file mode 100644 index 000000000..e8a74cadb --- /dev/null +++ b/tests/stream-transient-history.test.mjs @@ -0,0 +1,65 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createStreamTransientHistory } from '../modules/chat/streamTransientHistory.js'; + +test('transient stream history owns pending models without performing durable writes', async () => { + let current = [{ id: 'user-a', role: 'user', timestamp: 1 }]; + let durableReads = 0; + const provider = createStreamTransientHistory({ + repository: { + async getHistory() { durableReads += 1; return [{ id: 'user-b', role: 'user', timestamp: 3 }]; }, + async saveHistory() { assert.fail('transient provider must never perform a durable write'); }, + }, + currentHistory: { + get: () => current, + replace: history => { current = history; }, + }, + }); + + await provider.prepare({ id: 'assistant-a', replyToMessageId: 'user-a', timestamp: 2 }, { agentId: 'a', topicId: 'current' }, { visible: true }); + assert.equal(provider.pendingCount, 1); + assert.deepEqual(current.map(message => message.id), ['user-a', 'assistant-a']); + + const finalized = await provider.finalize({ + messageId: 'assistant-a', context: { agentId: 'a', topicId: 'current' }, + content: 'answer', finishReason: 'completed', visible: true, + }); + assert.equal(finalized.message.content, 'answer'); + assert.equal(finalized.message.isPendingStream, undefined); + assert.equal(provider.pendingCount, 0); + assert.equal(durableReads, 0); +}); + +test('background transient models read latest history and preserve reply ordering', async () => { + let current = [{ id: 'unrelated', timestamp: 99 }]; + const provider = createStreamTransientHistory({ + repository: { + async getHistory() { + return [ + { id: 'user-a', role: 'user', timestamp: 1 }, + { id: 'user-b', role: 'user', timestamp: 3 }, + ]; + }, + }, + currentHistory: { get: () => current, replace: history => { current = history; } }, + }); + const context = { agentId: 'background', topicId: 'topic' }; + await provider.prepare({ id: 'assistant-a', replyToMessageId: 'user-a', timestamp: 2 }, context, { visible: false }); + const terminal = await provider.finalize({ + messageId: 'assistant-a', context, content: 'done', finishReason: 'completed', visible: false, + }); + assert.deepEqual(terminal.history.map(message => message.id), ['user-a', 'assistant-a', 'user-b']); + assert.deepEqual(current.map(message => message.id), ['unrelated'], 'background projection must not replace current Surface history'); +}); + +test('dispose revokes transient history authority and clears pending models', async () => { + const provider = createStreamTransientHistory({ + repository: { async getHistory() { return []; } }, + currentHistory: { get: () => [], replace() {} }, + }); + await provider.prepare({ id: 'pending' }, { agentId: 'a', topicId: 't' }, { visible: true }); + provider.dispose(); + assert.equal(provider.pendingCount, 0); + await assert.rejects(provider.prepare({ id: 'late' }, { agentId: 'a', topicId: 't' }), /disposed/); + assert.equal(await provider.finalize({ messageId: 'pending' }), null); +}); diff --git a/tests/surface-conversation.test.mjs b/tests/surface-conversation.test.mjs new file mode 100644 index 000000000..21cfdbafe --- /dev/null +++ b/tests/surface-conversation.test.mjs @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createSurfaceConversation } from '../modules/chat/surfaceConversation.js'; + +test('SurfaceConversation owns a fixed identity and copies incoming history', () => { + const selectedItem = { id: 'agent-a', type: 'agent', name: 'A' }; + const source = [{ id: 'm1' }]; + const conversation = createSurfaceConversation({ selectedItem, topicId: 'topic-a', history: source }); + + selectedItem.id = 'agent-b'; + source.push({ id: 'outside' }); + assert.equal(conversation.selectedItemRef.get().id, 'agent-a'); + assert.equal(conversation.topicIdRef.get(), 'topic-a'); + assert.deepEqual(conversation.historyRef.get().map(message => message.id), ['m1']); + + const replacement = [{ id: 'm2' }]; + conversation.replaceHistory(replacement); + replacement.push({ id: 'outside-2' }); + assert.deepEqual(conversation.historyRef.get().map(message => message.id), ['m2']); +}); + +test('SurfaceConversation revokes mutation authority on dispose', () => { + const conversation = createSurfaceConversation({ selectedItem: { id: 'agent-a', type: 'agent' }, topicId: 'topic-a' }); + assert.equal(conversation.historyRef.set([{ id: 'm1' }]), true); + conversation.dispose(); + conversation.dispose(); + + assert.equal(conversation.isActive(), false); + assert.deepEqual(conversation.historyRef.get(), []); + assert.equal(conversation.historyRef.set([{ id: 'late' }]), false); + assert.throws(() => conversation.replaceHistory([{ id: 'late' }]), /disposed/); +}); diff --git a/tests/surface-task-owner.test.mjs b/tests/surface-task-owner.test.mjs new file mode 100644 index 000000000..88633a5ba --- /dev/null +++ b/tests/surface-task-owner.test.mjs @@ -0,0 +1,72 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createSurfaceTaskOwner } from '../modules/renderer/surfaceTaskOwner.js'; + +function createControlledScheduler() { + let id = 0; + const callbacks = new Map(); + return { + requestAnimationFrame(callback) { callbacks.set(++id, callback); return id; }, + cancelAnimationFrame(handle) { callbacks.delete(handle); }, + requestIdleCallback(callback) { callbacks.set(++id, callback); return id; }, + cancelIdleCallback(handle) { callbacks.delete(handle); }, + flush() { const work = [...callbacks.values()]; callbacks.clear(); work.forEach(callback => callback(0)); }, + get size() { return callbacks.size; }, + }; +} + +test('revoking one JSDOM Surface does not revoke another root', async () => { + const dom = new JSDOM('
          '); + const rootA = dom.window.document.getElementById('a'); + const rootB = dom.window.document.getElementById('b'); + const scheduler = createControlledScheduler(); + const owner = createSurfaceTaskOwner({ environmentForRoot: () => scheduler }); + const workA = owner.animationFrame(rootA, () => rootA.append('A complete')); + const workB = owner.animationFrame(rootB, () => rootB.append('B should not appear')); + + owner.revoke(rootB); + scheduler.flush(); + await Promise.all([workA, workB]); + assert.equal(rootA.textContent, 'A complete'); + assert.equal(rootB.textContent, ''); +}); + +test('dispose cancels scheduled work and waits for already-started work', async () => { + const dom = new JSDOM('
          '); + const root = dom.window.document.getElementById('a'); + const scheduler = createControlledScheduler(); + const owner = createSurfaceTaskOwner({ environmentForRoot: () => scheduler }); + let release; + const gate = new Promise(resolve => { release = resolve; }); + const running = owner.animationFrame(root, async () => { await gate; root.append('finished'); }); + scheduler.flush(); + let disposed = false; + const disposal = owner.dispose(root).then(() => { disposed = true; }); + await Promise.resolve(); + assert.equal(disposed, false); + release(); + await Promise.all([running, disposal]); + assert.equal(disposed, true); + + const late = owner.animationFrame(root, () => root.append('late')); + await assert.rejects(late, /disposed/); + assert.equal(root.textContent, 'finished'); + assert.equal(scheduler.size, 0); +}); + +test('frame fallback owns and clears its timeout handle', async () => { + let callback = null; + let cleared = null; + const environment = { + setTimeout(next) { callback = next; return 41; }, + clearTimeout(handle) { cleared = handle; callback = null; }, + }; + const owner = createSurfaceTaskOwner({ environmentForRoot: () => environment }); + const root = {}; + const work = owner.animationFrame(root, () => assert.fail('revoked fallback must not run')); + owner.revoke(root); + assert.equal(cleared, 41); + assert.equal(callback, null); + assert.deepEqual(await work, { status: 'revoked' }); +}); diff --git a/tests/topic-list-mode-lifecycle.test.js b/tests/topic-list-mode-lifecycle.test.js index deef40bbc..932802c47 100644 --- a/tests/topic-list-mode-lifecycle.test.js +++ b/tests/topic-list-mode-lifecycle.test.js @@ -34,12 +34,17 @@ test('legacy mode events cannot tear down canonical topic management state', asy window.topicListManager.init({ elements: { topicListContainer: window.document.getElementById('tabContentTopics') }, electronAPI: {}, + chatRepository: { getHistory: async () => [] }, refs: { currentSelectedItemRef: { get: () => ({ id: '', type: '' }) }, currentTopicIdRef: { get: () => 'topic-a' } }, uiHelper: {}, - mainRendererFunctions: {} + mainRendererFunctions: {}, + topicSelectionReadiness: { + isReady: () => true, + defer() {}, + } }); window.document.documentElement.dataset.uiMode = 'classic'; @@ -56,3 +61,30 @@ test('legacy mode events cannot tear down canonical topic management state', asy assert.equal(window.document.getElementById('nextUiManageTopicsBtn').getAttribute('aria-pressed'), 'true'); dom.window.close(); }); + +test('topic manager accepts a lifecycle listener owner and disposes its render resources', () => { + const dom = new JSDOM('
            ', { runScripts: 'outside-only' }); + const { window } = dom; + window.eval(fs.readFileSync(path.join(root, 'modules/topicListManager.js'), 'utf8')); + const removals = []; + const owner = { + add(target, type, handler, options) { + target.addEventListener(type, handler, options); + removals.push(() => target.removeEventListener(type, handler, options)); + return true; + }, + dispose() { removals.splice(0).reverse().forEach(remove => remove()); } + }; + window.topicListManager.init({ + elements: { topicListContainer: window.document.getElementById('tabContentTopics') }, + electronAPI: {}, + chatRepository: { getHistory: async () => [] }, + refs: { currentSelectedItemRef: { get: () => ({ id: '', type: '' }) }, currentTopicIdRef: { get: () => null } }, + uiHelper: {}, mainRendererFunctions: {}, topicSelectionReadiness: { isReady: () => true, defer() {} }, listenerOwner: owner + }); + window.topicListManager.dispose(); + owner.dispose(); + assert.equal(removals.length, 0); + assert.equal(typeof window.topicListManager.dispose, 'function'); + dom.window.close(); +}); diff --git a/tests/topic-selection-readiness.test.mjs b/tests/topic-selection-readiness.test.mjs new file mode 100644 index 000000000..56f3abe33 --- /dev/null +++ b/tests/topic-selection-readiness.test.mjs @@ -0,0 +1,26 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createTopicSelectionReadiness } from '../modules/renderer/topicSelectionReadiness.js'; + +test('topic selection readiness defers and consumes the latest selection without ambient state', () => { + const readiness = createTopicSelectionReadiness(); + assert.equal(readiness.isReady(), false); + readiness.defer({ itemId: 'agent-a', itemType: 'agent', topicId: 'topic-a' }); + readiness.defer({ itemId: 'agent-a', itemType: 'agent', topicId: 'topic-b' }); + assert.deepEqual(readiness.takePending(), { itemId: 'agent-a', itemType: 'agent', topicId: 'topic-b' }); + assert.equal(readiness.takePending(), null); + + readiness.setReady(true); + assert.equal(readiness.isReady(), true); + readiness.defer({ itemId: 'agent-a', itemType: 'agent', topicId: 'topic-c' }); + assert.deepEqual(readiness.takePending(), { itemId: 'agent-a', itemType: 'agent', topicId: 'topic-c' }); + readiness.setReady(false); + assert.equal(readiness.isReady(), false); +}); + +test('topic selection readiness clearPending revokes a queued selection', () => { + const readiness = createTopicSelectionReadiness(); + readiness.defer({ itemId: 'agent-a', itemType: 'agent', topicId: 'topic-a' }); + readiness.clearPending(); + assert.equal(readiness.takePending(), null); +}); diff --git a/tests/tts-surface-owner.test.mjs b/tests/tts-surface-owner.test.mjs new file mode 100644 index 000000000..2fb4ea5d3 --- /dev/null +++ b/tests/tts-surface-owner.test.mjs @@ -0,0 +1,70 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createTtsSurfaceOwner } from '../modules/renderer/ttsSurfaceOwner.js'; + +function createHarness() { + let play; + let stop; + let unsubscribed = 0; + const sources = []; + const indicators = []; + let resolveDecode; + const context = { + destination: {}, + decodeAudioData: () => new Promise(resolve => { resolveDecode = resolve; }), + createBufferSource: () => { + const source = { connect() {}, start() {}, stopCalled: 0, stop() { this.stopCalled += 1; } }; + sources.push(source); + return source; + }, + async close() {}, + }; + const owner = createTtsSurfaceOwner({ + subscribePlay: callback => { play = callback; return () => { unsubscribed += 1; }; }, + subscribeStop: callback => { stop = callback; return () => { unsubscribed += 1; }; }, + createAudioContext: () => context, + decodeBase64: () => new ArrayBuffer(1), + updateSpeakingIndicator: (id, active) => indicators.push([id, active]), + showError() {}, + }); + return { owner, emitPlay: value => play(value), emitStop: () => stop(), resolveDecode: value => resolveDecode(value), sources, indicators, get unsubscribed() { return unsubscribed; } }; +} + +test('TTS owner rejects late sessions and stops the prior source on replacement', async () => { + const harness = createHarness(); + harness.owner.mount(); + assert.equal(harness.owner.ensureAudioContext(), true); + harness.emitPlay({ audioData: 'a', msgId: 'm1', sessionId: 1 }); + harness.resolveDecode({}); + await Promise.resolve(); + await Promise.resolve(); + assert.equal(harness.sources.length, 1); + harness.emitPlay({ audioData: 'b', msgId: 'm2', sessionId: 2 }); + assert.equal(harness.sources[0].stopCalled, 1); + harness.emitPlay({ audioData: 'late', msgId: 'late', sessionId: 1 }); + harness.resolveDecode({}); + await harness.owner.dispose(); +}); + +test('TTS owner dispose unsubscribes, waits for decode, and silences late completion', async () => { + const harness = createHarness(); + harness.owner.mount(); + harness.owner.ensureAudioContext(); + harness.emitPlay({ audioData: 'a', msgId: 'm1', sessionId: 1 }); + const disposal = harness.owner.dispose(); + harness.resolveDecode({}); + await disposal; + assert.equal(harness.unsubscribed, 2); + assert.equal(harness.sources.length, 0); + assert.deepEqual(harness.indicators.at(-1), ['m1', false]); + assert.equal(harness.owner.ensureAudioContext(), false); +}); + +test('TTS owner mount and dispose are idempotent', async () => { + const harness = createHarness(); + harness.owner.mount(); + harness.owner.mount(); + await harness.owner.dispose(); + await harness.owner.dispose(); + assert.equal(harness.unsubscribed, 2); +}); diff --git a/tests/ui-manager-lifecycle.test.mjs b/tests/ui-manager-lifecycle.test.mjs new file mode 100644 index 000000000..6f5ae1196 --- /dev/null +++ b/tests/ui-manager-lifecycle.test.mjs @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { pathToFileURL } from 'node:url'; +import { JSDOM } from 'jsdom'; + +test('uiManager dispose is idempotent and revokes reinitialization', async () => { + const dom = new JSDOM('
            ', { + url: 'https://vcpchat.local/', + }); + const previous = { window: globalThis.window, document: globalThis.document }; + Object.assign(globalThis, { window: dom.window, document: dom.window.document }); + const moduleUrl = `${pathToFileURL(path.join(process.cwd(), 'modules/uiManager.js')).href}?lifecycle=${Date.now()}`; + try { + const { default: _unused, ...namespace } = await import(moduleUrl).catch(error => { + throw error; + }); + void namespace; + const manager = dom.window.uiManager; + assert.ok(manager, 'uiManager should expose its public facade'); + const api = { + setTheme() {}, + onThemeUpdated() { return () => {}; }, + getCurrentTheme: async () => 'light', + saveSettings: async () => ({ success: true }), + }; + await manager.init({ + electronAPI: api, + refs: { globalSettingsRef: { get: () => ({ currentThemeMode: 'light' }), set() {} } }, + listenerOwner: { capture: () => () => {}, timeout: () => null }, + elements: { + leftSidebar: dom.window.document.querySelector('.sidebar'), + rightNotificationsSidebar: dom.window.document.getElementById('notificationsSidebar'), + resizerLeft: null, resizerRight: null, + digitalClockElement: null, dateDisplayElement: null, notificationTitleElement: null, + sidebarTabButtons: [], sidebarTabContents: [], + }, + }); + await manager.dispose(); + await manager.dispose(); + await assert.rejects(() => manager.init({}), /disposed/i); + } finally { + if (previous.window === undefined) delete globalThis.window; + else globalThis.window = previous.window; + if (previous.document === undefined) delete globalThis.document; + else globalThis.document = previous.document; + dom.window.close(); + } +}); + +test('uiManager unsubscribes theme updates and suppresses a late initial theme result', async () => { + const dom = new JSDOM('
            ', { + url: 'https://vcpchat.local/', + }); + const previous = { window: globalThis.window, document: globalThis.document }; + Object.assign(globalThis, { window: dom.window, document: dom.window.document }); + let resolveTheme; + const pendingTheme = new Promise(resolve => { resolveTheme = resolve; }); + let themeListener; + let released = 0; + try { + dom.window.eval(fs.readFileSync(path.join(process.cwd(), 'modules/uiManager.js'), 'utf8')); + const manager = dom.window.uiManager; + const initializing = manager.init({ + electronAPI: { + onThemeUpdated(listener) { themeListener = listener; return () => { released += 1; }; }, + getCurrentTheme: () => pendingTheme, + saveSettings: async () => ({ success: true }), + }, + refs: { globalSettingsRef: { get: () => ({}), set() {} } }, + listenerOwner: { capture: () => () => {}, timeout: () => null }, + elements: { + leftSidebar: dom.window.document.querySelector('.sidebar'), + rightNotificationsSidebar: dom.window.document.getElementById('notificationsSidebar'), + resizerLeft: null, resizerRight: null, + digitalClockElement: null, dateDisplayElement: null, notificationTitleElement: null, + sidebarTabButtons: [], sidebarTabContents: [], + }, + }); + await new Promise(resolve => setImmediate(resolve)); + let disposed = false; + const disposal = manager.dispose().then(() => { disposed = true; }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(disposed, false); + resolveTheme('dark'); + await Promise.all([initializing, disposal]); + themeListener?.('dark'); + assert.equal(released, 1); + assert.equal(dom.window.document.body.classList.contains('dark-theme'), false); + } finally { + if (previous.window === undefined) delete globalThis.window; + else globalThis.window = previous.window; + if (previous.document === undefined) delete globalThis.document; + else globalThis.document = previous.document; + dom.window.close(); + } +}); diff --git a/tests/vcp-stream-bridge.test.mjs b/tests/vcp-stream-bridge.test.mjs new file mode 100644 index 000000000..80e8c6035 --- /dev/null +++ b/tests/vcp-stream-bridge.test.mjs @@ -0,0 +1,261 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createVcpStreamBridge } from '../modules/chat/vcpStreamBridge.js'; +import { createMainChatStreamConsumer } from '../modules/renderer/mainChatStreamConsumer.js'; + +const waitUntil = async predicate => { + for (let attempt = 0; attempt < 50; attempt += 1) { + if (predicate()) return; + await Promise.resolve(); + } + assert.fail('stream bridge did not reach its real terminal promise'); +}; + +test('VCP bridge drives one consumer from thinking through persisted terminal', async () => { + const seen = []; + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + normalizeChunk: chunk => chunk.choices[0].delta.content, + prepare: event => seen.push(`prepare:${event.type}`), + consume: event => seen.push(`event:${event.type}`), + persist: value => { seen.push(`persist:${value.terminal.kind}`); return { content: value.snapshot.text }; }, + }), + }); + assert.equal(bridge.accept({ type: 'agent_thinking', messageId: 'm1', context: { agentId: 'a', topicId: 't' } }), true); + bridge.accept({ type: 'start', messageId: 'm1', context: { agentId: 'a', topicId: 't' } }); + bridge.accept({ type: 'data', messageId: 'm1', context: { agentId: 'a', topicId: 't' }, chunk: { choices: [{ delta: { content: 'hello' } }] } }); + bridge.accept({ type: 'end', messageId: 'm1', context: { agentId: 'a', topicId: 't' }, finish_reason: 'stop' }); + await waitUntil(() => seen.includes('event:completed')); + assert.deepEqual(seen, [ + 'event:started', 'prepare:agent_thinking', 'prepare:start', 'prepare:data', + 'event:chunk', 'prepare:end', 'persist:completed', 'event:completed', + ]); + await bridge.dispose(); +}); + +test('VCP bridge ignores unrelated events and normalizes error once', async () => { + const terminals = []; + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + prepare() {}, + consume: event => { + if (['completed', 'failed', 'cancelled', 'discarded'].includes(event.type)) terminals.push(event); + }, + persist() {}, + }), + }); + assert.equal(bridge.accept({ type: 'remove_message', messageId: 'm2' }), false); + bridge.accept({ type: 'data', messageId: 'm2', context: { groupId: 'g', topicId: 't' }, chunk: 'partial' }); + bridge.accept({ type: 'error', messageId: 'm2', context: { groupId: 'g', topicId: 't' }, error: 'offline' }); + await waitUntil(() => terminals.length === 1); + assert.equal(terminals[0].type, 'failed'); + assert.equal(terminals[0].outcome.transport.kind, 'failed'); + await bridge.dispose(); +}); + +test('VCP bridge dispose aborts an externally-driven operation and reaches quiescence', async () => { + const seen = []; + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + prepare() {}, + consume: event => seen.push(event.type), + persist() { throw new Error('discarded operations must not persist'); }, + }), + }); + bridge.accept({ type: 'data', messageId: 'pending', context: { agentId: 'a', topicId: 't' }, chunk: 'partial' }); + await bridge.dispose(); + assert.deepEqual(seen, ['started'], 'dispose detaches the consumer before queued projection work can publish'); +}); + +test('one operation can be disposed without a producer terminal and rejects every late event', async () => { + const seen = []; + let consumerDisposals = 0; + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + prepare: event => seen.push(`prepare:${event.type}`), + consume: event => seen.push(`event:${event.type}`), + persist() { assert.fail('a retracted operation must not persist'); }, + dispose() { consumerDisposals += 1; }, + }), + }); + assert.equal(bridge.accept({ type: 'data', messageId: 'owned', context: { agentId: 'a', topicId: 't' }, chunk: 'partial' }), true); + await waitUntil(() => seen.includes('event:chunk')); + const outcome = await bridge.disposeOperation('owned'); + assert.equal(outcome.kind, 'discarded'); + assert.equal(consumerDisposals, 1); + assert.equal(bridge.accept({ type: 'end', messageId: 'owned', context: { agentId: 'a', topicId: 't' } }), false); + assert.equal(bridge.accept({ type: 'data', messageId: 'owned', context: { agentId: 'a', topicId: 't' }, chunk: 'late' }), false); + await bridge.dispose(); +}); + +test('retiring before the first producer event prevents operation creation and stream resurrection', async () => { + let consumersCreated = 0; + const bridge = createVcpStreamBridge({ + createConsumer: () => { + consumersCreated += 1; + return {}; + }, + }); + assert.equal(await bridge.disposeOperation('before-first-event'), false); + assert.equal(bridge.accept({ type: 'data', messageId: 'before-first-event', context: { agentId: 'a', topicId: 't' }, chunk: 'late' }), false); + assert.equal(bridge.accept({ type: 'end', messageId: 'before-first-event', context: { agentId: 'a', topicId: 't' } }), false); + assert.equal(consumersCreated, 0); + + assert.equal(await bridge.cancelOperation('cancel-before-first-event'), null); + assert.equal(bridge.accept({ type: 'start', messageId: 'cancel-before-first-event', context: { agentId: 'a', topicId: 't' } }), false); + assert.equal(consumersCreated, 0); + await bridge.dispose(); +}); + +test('producer operation identity isolates a same-message retry from delayed events', async () => { + const seen = []; + const bridge = createVcpStreamBridge({ + createConsumer: event => ({ + prepare() {}, + consume: item => seen.push(`${event.streamOperationId}:${item.type}`), + persist() {}, + }), + }); + bridge.accept({ type: 'data', messageId: 'same-message', streamOperationId: 'attempt-1', context: { agentId: 'a', topicId: 't' }, chunk: 'one' }); + bridge.accept({ type: 'end', messageId: 'same-message', streamOperationId: 'attempt-1', context: { agentId: 'a', topicId: 't' } }); + await waitUntil(() => seen.includes('attempt-1:completed')); + bridge.accept({ type: 'data', messageId: 'same-message', streamOperationId: 'attempt-2', context: { agentId: 'a', topicId: 't' }, chunk: 'two' }); + assert.equal(bridge.accept({ type: 'data', messageId: 'same-message', streamOperationId: 'attempt-1', context: { agentId: 'a', topicId: 't' }, chunk: 'late-old' }), false); + bridge.accept({ type: 'end', messageId: 'same-message', streamOperationId: 'attempt-2', context: { agentId: 'a', topicId: 't' } }); + await waitUntil(() => seen.includes('attempt-2:completed')); + assert.equal(seen.some(item => item.includes('late-old')), false); + await bridge.dispose(); +}); + +test('local cancellation drains accepted chunks and persists one cancelled terminal', async () => { + const seen = []; + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + prepare: event => seen.push(`prepare:${event.type}`), + consume: event => seen.push(`event:${event.type}`), + persist: value => { seen.push(`persist:${value.terminal.kind}`); }, + }), + }); + bridge.accept({ type: 'data', messageId: 'cancel-local', context: { agentId: 'a', topicId: 't' }, chunk: 'partial' }); + const outcome = await bridge.cancelOperation('cancel-local', 'backend-interrupt-failed'); + assert.equal(outcome.kind, 'cancelled'); + assert.deepEqual(seen, [ + 'event:started', 'prepare:data', 'event:chunk', 'persist:cancelled', 'event:cancelled', + ]); + assert.equal(bridge.accept({ type: 'end', messageId: 'cancel-local', context: { agentId: 'a', topicId: 't' } }), false); + await bridge.dispose(); +}); + +test('VCP bridge serializes deferred prepare before immediate chunk and terminal', async () => { + const seen = []; + let releasePrepare; + const prepareGate = new Promise(resolve => { releasePrepare = resolve; }); + const bridge = createVcpStreamBridge({ + createConsumer: () => ({ + async prepare(event) { + seen.push(`prepare-start:${event.type}`); + if (event.type === 'data') await prepareGate; + seen.push(`prepare-end:${event.type}`); + }, + consume: event => seen.push(`event:${event.type}`), + persist: value => { seen.push(`persist:${value.terminal.kind}`); }, + }), + }); + bridge.accept({ type: 'data', messageId: 'fast', context: { agentId: 'a', topicId: 't' }, chunk: 'x' }); + bridge.accept({ type: 'end', messageId: 'fast', context: { agentId: 'a', topicId: 't' } }); + await Promise.resolve(); + assert.deepEqual(seen, ['event:started', 'prepare-start:data']); + releasePrepare(); + await waitUntil(() => seen.includes('event:completed')); + assert.deepEqual(seen, [ + 'event:started', 'prepare-start:data', 'prepare-end:data', 'event:chunk', + 'prepare-start:end', 'prepare-end:end', 'persist:completed', 'event:completed', + ]); + await bridge.dispose(); +}); + +test('projection failure silences already queued events and cannot commit a partial terminal', async () => { + const seen = []; + // Fail the first projection before queuing an end event synchronously. + const failingBridge = createVcpStreamBridge({ + reportError: () => {}, + createConsumer: event => createMainChatStreamConsumer(event, { + start: async () => { throw new Error('controlled projection failure'); }, + append: () => {}, + projectTerminal: async () => { seen.push('unexpected-project-terminal'); return { id: event.messageId }; }, + persistTerminal: async () => { seen.push('unexpected-persist-terminal'); }, + }), + }); + failingBridge.accept({ type: 'data', messageId: 'projection-failure', context: { agentId: 'a', topicId: 't' }, chunk: 'x' }); + failingBridge.accept({ type: 'end', messageId: 'projection-failure', context: { agentId: 'a', topicId: 't' } }); + await new Promise(resolve => setTimeout(resolve, 0)); + assert.equal(seen.includes('unexpected-project-terminal'), false); + assert.equal(seen.includes('unexpected-persist-terminal'), false); + await failingBridge.dispose(); +}); + +test('missing terminal projection becomes one persistence failure instead of a completed no-op', async () => { + const terminals = []; + const bridge = createVcpStreamBridge({ + reportError: () => {}, + createConsumer: event => { + const consumer = createMainChatStreamConsumer(event, { + start() {}, + append() {}, + async projectTerminal() { return null; }, + async persistTerminal() { assert.fail('a missing projection must not be committed'); }, + renderError() {}, + dispatchTerminal() {}, + getSurfaceGeneration: () => 0, + }); + return Object.freeze({ + ...consumer, + consume(streamEvent) { + if (['completed', 'failed', 'cancelled', 'discarded'].includes(streamEvent.type)) terminals.push(streamEvent); + consumer.consume(streamEvent); + }, + }); + }, + }); + bridge.accept({ type: 'end', messageId: 'missing', context: { agentId: 'a', topicId: 't' } }); + await waitUntil(() => terminals.length === 1); + assert.equal(terminals[0].type, 'failed'); + assert.equal(terminals[0].outcome.phase, 'persistence'); + await bridge.dispose(); +}); + +test('post-commit side effect failure is reported without changing the durable completed outcome', async () => { + const terminals = []; + const reports = []; + let committed = false; + const bridge = createVcpStreamBridge({ + reportError: (...args) => reports.push(args), + createConsumer: event => { + const consumer = createMainChatStreamConsumer(event, { + start() {}, + append() {}, + async projectTerminal() { + return { messageId: 'post-commit', context: event.context, history: [], finishReason: 'completed' }; + }, + async persistTerminal(value) { committed = true; return value; }, + async afterPersist() { throw new Error('summary failed'); }, + reportError: (...args) => reports.push(args), + getSurfaceGeneration: () => 0, + }); + return Object.freeze({ + ...consumer, + consume(streamEvent) { + if (['completed', 'failed', 'cancelled', 'discarded'].includes(streamEvent.type)) terminals.push(streamEvent); + consumer.consume(streamEvent); + }, + }); + }, + }); + bridge.accept({ type: 'end', messageId: 'post-commit', context: { agentId: 'a', topicId: 't' } }); + await waitUntil(() => terminals.length === 1); + assert.equal(committed, true); + assert.equal(terminals[0].type, 'completed'); + assert.equal(terminals[0].outcome.persistence.status, 'saved'); + assert.equal(reports.some(([, error]) => error?.message === 'summary failed'), true); + await bridge.dispose(); +}); diff --git a/tests/visibility-optimizer-owner.test.mjs b/tests/visibility-optimizer-owner.test.mjs new file mode 100644 index 000000000..00bf7620e --- /dev/null +++ b/tests/visibility-optimizer-owner.test.mjs @@ -0,0 +1,102 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; +import { createVisibilityOptimizer } from '../modules/renderer/visibilityOptimizer.js'; + +test('visibility owners share only the realm interceptor and dispose independently', () => { + const dom = new JSDOM('
            '); + const previous = { window: globalThis.window, Element: globalThis.Element, IntersectionObserver: globalThis.IntersectionObserver, MutationObserver: globalThis.MutationObserver, requestAnimationFrame: globalThis.requestAnimationFrame }; + const observed = new Set(); + class FakeIntersectionObserver { + observe(value) { observed.add(value); } + unobserve(value) { observed.delete(value); } + disconnect() {} + } + globalThis.window = dom.window; + globalThis.Element = dom.window.Element; + globalThis.IntersectionObserver = FakeIntersectionObserver; + globalThis.MutationObserver = dom.window.MutationObserver; + globalThis.requestAnimationFrame = callback => { callback(); return 1; }; + const nativeAnimate = () => ({ playState: 'running', pauseCalls: 0, pause() { this.pauseCalls += 1; }, play() {} }); + dom.window.Element.prototype.animate = nativeAnimate; + try { + const rootA = dom.window.document.getElementById('a'); + const rootB = dom.window.document.getElementById('b'); + const messageA = rootA.querySelector('.message-item'); + const messageB = rootB.querySelector('.message-item'); + const ownerA = createVisibilityOptimizer(); + const ownerB = createVisibilityOptimizer(); + ownerA.initializeVisibilityOptimizer(rootA); + ownerB.initializeVisibilityOptimizer(rootB); + const animationA = messageA.querySelector('span').animate([], {}); + const animationB = messageB.querySelector('span').animate([], {}); + ownerA.pauseMessageAnimations(messageA); + ownerB.pauseMessageAnimations(messageB); + assert.equal(animationA.pauseCalls, 1); + assert.equal(animationB.pauseCalls, 1); + + ownerA.destroyVisibilityOptimizer(); + assert.notEqual(dom.window.Element.prototype.animate, nativeAnimate, 'disposing A must not remove B realm interceptor'); + const laterB = messageB.querySelector('span').animate([], {}); + ownerB.pauseMessageAnimations(messageB); + assert.equal(laterB.pauseCalls, 1, 'disposing A must retain B interceptor ownership'); + ownerB.destroyVisibilityOptimizer(); + assert.equal(dom.window.Element.prototype.animate, nativeAnimate, 'last owner must restore native realm method'); + assert.equal(observed.size, 0); + } finally { + globalThis.window = previous.window; + globalThis.Element = previous.Element; + globalThis.IntersectionObserver = previous.IntersectionObserver; + globalThis.MutationObserver = previous.MutationObserver; + globalThis.requestAnimationFrame = previous.requestAnimationFrame; + dom.window.close(); + } +}); + +test('dispose cancels the owned delayed scan before it can touch detached content', () => { + const dom = new JSDOM('
            '); + const previous = { window: globalThis.window, Element: globalThis.Element, IntersectionObserver: globalThis.IntersectionObserver, MutationObserver: globalThis.MutationObserver, requestAnimationFrame: globalThis.requestAnimationFrame }; + const pendingTimers = new Map(); + let nextTimerId = 1; + class FakeIntersectionObserver { + observe() {} + unobserve() {} + disconnect() {} + } + const originalSetTimeout = dom.window.setTimeout.bind(dom.window); + const originalClearTimeout = dom.window.clearTimeout.bind(dom.window); + dom.window.setTimeout = callback => { + const id = nextTimerId++; + pendingTimers.set(id, callback); + return id; + }; + dom.window.clearTimeout = id => pendingTimers.delete(id); + globalThis.window = dom.window; + globalThis.Element = dom.window.Element; + globalThis.IntersectionObserver = FakeIntersectionObserver; + globalThis.MutationObserver = dom.window.MutationObserver; + globalThis.requestAnimationFrame = callback => { callback(); return 1; }; + dom.window.Element.prototype.animate = () => ({ playState: 'running', pause() {}, play() {} }); + try { + const root = dom.window.document.getElementById('root'); + const message = root.querySelector('.message-item'); + let scans = 0; + message.getAnimations = () => { scans += 1; return []; }; + const owner = createVisibilityOptimizer(); + owner.initializeVisibilityOptimizer(root); + assert.equal(pendingTimers.size, 1); + owner.destroyVisibilityOptimizer(); + assert.equal(pendingTimers.size, 0, 'dispose must cancel the Surface-owned delayed scan'); + for (const callback of pendingTimers.values()) callback(); + assert.equal(scans, 0); + } finally { + dom.window.setTimeout = originalSetTimeout; + dom.window.clearTimeout = originalClearTimeout; + globalThis.window = previous.window; + globalThis.Element = previous.Element; + globalThis.IntersectionObserver = previous.IntersectionObserver; + globalThis.MutationObserver = previous.MutationObserver; + globalThis.requestAnimationFrame = previous.requestAnimationFrame; + dom.window.close(); + } +}); diff --git a/tests/window-stream-runtime.test.mjs b/tests/window-stream-runtime.test.mjs new file mode 100644 index 000000000..15abccba2 --- /dev/null +++ b/tests/window-stream-runtime.test.mjs @@ -0,0 +1,100 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { JSDOM } from 'jsdom'; +import { createWindowStreamRuntime } from '../modules/renderer/windowStreamRuntime.js'; + +const rootDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +test('auxiliary window stream runtime owns success, persistence and disposal', async () => { + const root = new JSDOM('
            ').window.document.querySelector('div'); + const calls = []; + let settle; + const settled = new Promise(resolve => { settle = resolve; }); + const runtime = createWindowStreamRuntime({ + root, + streamProjection: { + startStreamingMessage: message => { calls.push(['start', message.name]); }, + appendStreamChunk: (_id, chunk) => { calls.push(['chunk', chunk]); }, + projectStreamTerminal: async (_id, reason) => ({ messageId: 'm1', finishReason: reason, context: { agentId: 'a', topicId: 'voicechat_a' }, history: [] }), + }, + historyPersistence: { + commit: async value => { calls.push(['persist', value.finishReason]); return value; }, + }, + getMessageContext: () => ({ agentName: 'Voice Agent' }), + contextFilter: context => context?.topicId === 'voicechat_a', + afterPersist: value => { calls.push(['terminal', value.terminal.kind]); }, + onSettled: ({ event }) => { calls.push(['settled', event.type]); settle(); }, + }); + assert.equal(runtime.accept({ type: 'data', messageId: 'm1', chunk: 'hello', context: { agentId: 'a', topicId: 'voicechat_a' } }), true); + assert.equal(runtime.accept({ type: 'end', messageId: 'm1', context: { agentId: 'a', topicId: 'voicechat_a' } }), true); + await settled; + assert.deepEqual(calls, [ + ['start', 'Voice Agent'], ['chunk', 'hello'], ['persist', 'completed'], ['terminal', 'completed'], ['settled', 'completed'], + ]); + assert.equal(runtime.accept({ type: 'data', messageId: 'other', context: { topicId: 'other' } }), false); + await runtime.dispose(); +}); + +test('auxiliary composer settlement still runs when durable commit fails', async () => { + const root = new JSDOM('
            ').window.document.querySelector('div'); + let settledType = null; + const runtime = createWindowStreamRuntime({ + root, + streamProjection: { + startStreamingMessage() {}, + appendStreamChunk() {}, + async projectStreamTerminal() { + return { messageId: 'm2', finishReason: 'completed', context: { agentId: 'a', topicId: 'voicechat_a' }, history: [] }; + }, + }, + historyPersistence: { async commit() { throw new Error('disk failed'); } }, + contextFilter: context => context?.topicId === 'voicechat_a', + onSettled: ({ event }) => { settledType = event.type; }, + reportError: () => {}, + }); + runtime.accept({ type: 'end', messageId: 'm2', context: { agentId: 'a', topicId: 'voicechat_a' } }); + for (let attempt = 0; attempt < 50 && settledType === null; attempt += 1) await Promise.resolve(); + assert.equal(settledType, 'failed'); + await runtime.dispose(); +}); + +test('auxiliary window close cancellation waits for the operation terminal', async () => { + const root = new JSDOM('
            ').window.document.querySelector('div'); + const calls = []; + const runtime = createWindowStreamRuntime({ + root, + streamProjection: { + startStreamingMessage() { calls.push('start'); }, + appendStreamChunk() { calls.push('chunk'); }, + async projectStreamTerminal(_id, reason) { + calls.push(`project:${reason}`); + return { messageId: 'm3', finishReason: reason, context: { agentId: 'a', topicId: 'assistant_chat' }, history: [] }; + }, + }, + historyPersistence: { async commit(value) { calls.push('persist'); return value; } }, + contextFilter: context => context?.topicId === 'assistant_chat', + onSettled: ({ event }) => calls.push(`settled:${event.type}`), + }); + runtime.accept({ type: 'data', messageId: 'm3', chunk: 'partial', context: { agentId: 'a', topicId: 'assistant_chat' } }); + const outcome = await runtime.cancel('m3', 'assistant-window-close'); + assert.equal(outcome.kind, 'cancelled'); + assert.deepEqual(calls, ['start', 'chunk', 'project:cancelled', 'persist', 'settled:cancelled']); + assert.equal(runtime.accept({ type: 'end', messageId: 'm3', context: { agentId: 'a', topicId: 'assistant_chat' } }), false); + await runtime.dispose(); +}); + +test('voice and assistant windows cancel the real operation before durable close save', () => { + for (const relativePath of ['Voicechatmodules/voicechat.js', 'rust_assistant_engine/ui/assistant.js']) { + const source = fs.readFileSync(path.join(rootDirectory, relativePath), 'utf8'); + assert.match(source, /createTransientChatHistoryPersistence/); + assert.match(source, /await streamRuntime\?\.cancel\(activeStreamingMessageId,/); + assert.match(source, /let historyMutationAuthority = null/); + assert.match(source, /historyMutationAuthority = createChatHistoryMutationAuthority/); + assert.match(source, /await historyMutationAuthority\?\.dispose\(\)/); + assert.doesNotMatch(source, /const historyMutationAuthority = createChatHistoryMutationAuthority/); + assert.doesNotMatch(source, /waitForActiveStreamToSettle|Timed out while waiting stream to settle/); + } +});