feat: add copy action to AI responses

Completed AI responses now expose a bottom copy action that copies their Markdown source without reasoning, tool logs, or citation metadata. Clipboard writes use a validated Preload/Main bridge so sandboxed Electron sessions no longer fail with a renderer clipboard permission error; full-conversation copy uses the same path.

Release note: AI 回复底部现在可以直接复制 Markdown 原文,并修复了沙箱环境下回复和完整会话可能无法写入剪贴板的问题。
This commit is contained in:
mesalogo
2026-09-02 16:35:51 +08:00
parent 4d9ab72778
commit 42cbfa43f1
15 changed files with 243 additions and 19 deletions
+28 -1
View File
@@ -242,6 +242,29 @@ describe('terminal IPC boundary', () => {
snapshot.sessionId
)
electronMocks.handlers.get(ipcChannels.clipboardWriteText)?.(
event,
'## Copied response'
)
expect(electronMocks.writeClipboardText).toHaveBeenCalledWith(
'## Copied response'
)
expect(() =>
electronMocks.handlers.get(ipcChannels.clipboardWriteText)?.(
event,
{ text: 'invalid' }
)
).toThrow()
expect(() =>
electronMocks.handlers.get(ipcChannels.clipboardWriteText)?.(
{
sender: { ...webContents, id: 99 },
senderFrame: webContents.mainFrame
},
'untrusted'
)
).toThrow('拒绝来自未知窗口的 IPC 请求')
expect(() =>
electronMocks.handlers.get(ipcChannels.terminalWrite)?.(
{
@@ -295,7 +318,8 @@ const electronMocks = vi.hoisted(() => {
})),
openPath: vi.fn(async () => ''),
showItemInFolder: vi.fn(),
openExternal: vi.fn(async () => undefined)
openExternal: vi.fn(async () => undefined),
writeClipboardText: vi.fn()
}
})
@@ -1106,6 +1130,9 @@ vi.mock('electron', () => ({
getVersion: vi.fn(() => '0.1.0')
},
BrowserWindow: class {},
clipboard: {
writeText: electronMocks.writeClipboardText
},
dialog: {
showOpenDialog: electronMocks.showOpenDialog,
showSaveDialog: electronMocks.showSaveDialog
+7
View File
@@ -1,6 +1,7 @@
import {
app,
BrowserWindow,
clipboard,
dialog,
ipcMain,
shell
@@ -31,6 +32,7 @@ import {
browserReloadRequestSchema,
browserStopLoadingRequestSchema,
browserStopRequestSchema,
clipboardTextSchema,
conversationQueueUserInputSchema,
defaultRuntimeSettings,
knowledgeCreateSchema,
@@ -3298,6 +3300,11 @@ export function registerIpcHandlers(
}
})
registerHandler(ipcChannels.clipboardWriteText, (event, input) => {
assertTrustedSender(event, window)
clipboard.writeText(clipboardTextSchema.parse(input))
})
registerHandler(
ipcChannels.appRendererPersistenceReady,
(event) => {