Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7de0176
fix(native-chat): an unsent draft survives a reload or quit
brennanb2025 Oct 2, 2026
ea7f245
fix(native-chat): save draft images outside the state updater
brennanb2025 Oct 2, 2026
a483c65
fix(native-chat): one store owns each unsent draft
brennanb2025 Oct 3, 2026
809147e
test(native-chat): returned text is stored before its images arrive
brennanb2025 Oct 3, 2026
7b19d0f
fix(native-chat): keep returned text and drop launch-seed copies acro…
brennanb2025 Oct 3, 2026
9abc756
test(native-chat): a plain 260k given-back message stays saved
brennanb2025 Oct 3, 2026
ee415b3
fix(native-chat): saved drafts drop pasted images and die with their …
brennanb2025 Oct 3, 2026
ae0421c
fix(native-chat): the composer renders the draft store instead of kee…
brennanb2025 Oct 3, 2026
951e7ba
fix(native-chat): keep images pasted during a command and on shown co…
brennanb2025 Oct 3, 2026
9a7f9c9
fix(native-chat): a draft being written is never evicted from memory
brennanb2025 Oct 3, 2026
407ce4a
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 4, 2026
7336174
fix(native-chat): restored images Orca couldn't keep come back to att…
brennanb2025 Oct 4, 2026
f7177f2
test(native-chat): no send while an image waits to be attached again
brennanb2025 Oct 4, 2026
13672f8
fix(native-chat): pasted images in a draft come back after a restart
brennanb2025 Oct 4, 2026
657496c
test(native-chat): the paste sweep test ages the link itself
brennanb2025 Oct 4, 2026
a938777
fix(native-chat): type the paste-folder path helper with PlatformPath
brennanb2025 Oct 4, 2026
ad7b1a9
fix(native-chat): type the paste-folder path helper from path.posix
brennanb2025 Oct 4, 2026
5ff07e6
fix(native-chat): placeholder chips say what happened and only a user…
brennanb2025 Oct 4, 2026
0971ba9
fix(native-chat): only composer pastes use the paste folder, and rest…
brennanb2025 Oct 4, 2026
c640f75
fix(native-chat): a restored paste is readable by the path its draft …
brennanb2025 Oct 4, 2026
16c61f6
fix(native-chat): grant a paste's stored spelling only when it names …
brennanb2025 Oct 4, 2026
4b9f91a
test(native-chat): check the outside file by its real path too
brennanb2025 Oct 4, 2026
edd2436
fix(native-chat): a structured chat's draft belongs to its conversation
brennanb2025 Oct 4, 2026
a46fee3
fix(native-chat): every path that drops a worktree's tabs deletes the…
brennanb2025 Oct 4, 2026
506804c
fix(native-chat): only a user's delete removes a workspace's chat drafts
brennanb2025 Oct 4, 2026
c26a852
feat(native-chat): drafts live in IndexedDB with no budget, and a ref…
brennanb2025 Oct 4, 2026
0250a3d
fix(native-chat): drafts set from the store are never saved back, and…
brennanb2025 Oct 4, 2026
47312ed
test(native-chat): an early append the load already read is not added…
brennanb2025 Oct 4, 2026
2fc2929
test(native-chat): the store-value editor test waits for the editor a…
brennanb2025 Oct 4, 2026
393eb7f
fix(native-chat): early appends are read-modify-writes, and the load …
brennanb2025 Oct 4, 2026
9c0bda2
Merge origin/main (51fe6f3fbad) into chat-drafts-survive-reload
brennanb2025 Oct 5, 2026
a1fd9df
fix(native-chat): text given back is durable before its source is del…
brennanb2025 Oct 5, 2026
02163b3
test(native-chat): a kept paste is readable by the composer preview t…
brennanb2025 Oct 5, 2026
8b6c8c5
fix(native-chat): a per-scope write confirm, and the paste rule as C2…
brennanb2025 Oct 5, 2026
873711a
test(native-chat): the per-scope confirm settles at the IndexedDB tra…
brennanb2025 Oct 5, 2026
36f4664
fix(native-chat): a journaled removal always fits, and a load replays…
brennanb2025 Oct 5, 2026
5d2123c
Merge origin/main (0a8c5722f34, with #24917) into chat-drafts-survive…
brennanb2025 Oct 5, 2026
071c934
fix(native-chat): a chat with a draft is never reused or taken over
brennanb2025 Oct 5, 2026
99b7ec8
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
3590485
fix(native-chat): a returned text counts as already there only as its…
brennanb2025 Oct 5, 2026
8df9de2
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
b6b576b
fix(native-chat): a hand-back the draft already holds stays safe unti…
brennanb2025 Oct 5, 2026
20fbbd7
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
3b2ed2b
fix(lint): import worktree removal names once in the delete failure t…
brennanb2025 Oct 5, 2026
9240ee4
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
1565a18
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
554c48c
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
9a576cc
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-dra…
brennanb2025 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/window/clipboard-dashboard-popout-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ describe('dashboard popout clipboard access', () => {
expect(() => handlers.get('clipboard:readFilePaths')?.(popoutEvent)).toThrow(
'Unauthorized clipboard IPC sender'
)
expect(() =>
handlers.get('clipboard:restoreNativeChatPastes')?.(popoutEvent, ['/etc/passwd'])
).toThrow('Unauthorized clipboard IPC sender')
expect(() =>
handlers.get('clipboard:writeFile')?.(popoutEvent, {
filePath: '/tmp/copied-file.txt',
Expand Down
32 changes: 27 additions & 5 deletions src/main/window/clipboard-image-temp-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { dirname, join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { writeFileMock, getPathMock, writeFileBase64Mock } = vi.hoisted(() => ({
const { writeFileMock, mkdirMock, getPathMock, writeFileBase64Mock } = vi.hoisted(() => ({
writeFileMock: vi.fn(),
getPathMock: vi.fn(() => '/var/folders/ab/T'),
mkdirMock: vi.fn(),
getPathMock: vi.fn((name: string) =>
name === 'temp' ? '/os/temp' : '/Users/me/Library/Application Support/orca'
),
writeFileBase64Mock: vi.fn()
}))

vi.mock('node:fs/promises', () => ({ default: { writeFile: writeFileMock } }))
vi.mock('node:fs/promises', () => ({ default: { writeFile: writeFileMock, mkdir: mkdirMock } }))
vi.mock('node:crypto', () => ({ randomUUID: () => 'uuid-1' }))
vi.mock('../../shared/app-environment', () => ({
getAppEnvironment: () => ({ getPath: getPathMock })
Expand All @@ -25,10 +29,28 @@ beforeEach(() => {
})

describe('saveClipboardImageBufferAsTempFile', () => {
it('writes the pasted image to the local temp folder', async () => {
it('keeps a terminal, editor or phone paste in OS temp, as before', async () => {
const savedPath = await saveClipboardImageBufferAsTempFile(Buffer.from([1, 2, 3]))

expect(savedPath.startsWith('/var/folders/ab/T')).toBe(true)
expect(getPathMock).toHaveBeenCalledWith('temp')
expect(getPathMock).not.toHaveBeenCalledWith('userData')
expect(mkdirMock).not.toHaveBeenCalled()
expect(dirname(savedPath)).toBe('/os/temp')
expect(writeFileMock).toHaveBeenCalledWith(savedPath, Buffer.from([1, 2, 3]))
})

it('writes a native-chat composer paste into the paste folder, where its draft can find it', async () => {
const savedPath = await saveClipboardImageBufferAsTempFile(Buffer.from([1, 2, 3]), {
forNativeChatDraft: true
})

expect(mkdirMock).toHaveBeenCalledWith(
join('/Users/me/Library/Application Support/orca', 'native-chat-pastes'),
{ recursive: true }
)
expect(dirname(savedPath)).toBe(
join('/Users/me/Library/Application Support/orca', 'native-chat-pastes')
)
expect(writeFileMock).toHaveBeenCalledWith(savedPath, Buffer.from([1, 2, 3]))
})

Expand Down
14 changes: 12 additions & 2 deletions src/main/window/clipboard-image-temp-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { randomUUID } from 'node:crypto'

import { getAppEnvironment } from '../../shared/app-environment'
import { requireSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
import { getAppEnvironment } from '../../shared/app-environment'
import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path'
import { assertClipboardImageByteLengthWithinLimit } from '../../shared/clipboard-image'
import { nativeChatPasteFolder } from './native-chat-paste-files'

export type SaveClipboardImageAsTempFileArgs = {
connectionId?: string | null
runtimeEnvironmentId?: string | null
/** A native-chat composer paste: kept in Orca's paste folder so its draft can bring it back. */
forNativeChatDraft?: boolean
}

const REMOTE_CLIPBOARD_IMAGE_TEMP_DIR = '/tmp'
Expand Down Expand Up @@ -39,7 +42,14 @@ export async function saveClipboardImageBufferAsTempFile(
return remotePath
}

const tempPath = path.join(getAppEnvironment().getPath('temp'), fileName)
// Why only a composer paste goes to the paste folder: its draft can bring it back after a
// restart, while terminal, editor and phone pastes stay in OS temp, as they always have.
let folder = getAppEnvironment().getPath('temp')
if (args?.forNativeChatDraft === true) {
folder = nativeChatPasteFolder()
await fs.mkdir(folder, { recursive: true })
}
const tempPath = path.join(folder, fileName)
await fs.writeFile(tempPath, buffer)
return tempPath
}
4 changes: 1 addition & 3 deletions src/main/window/clipboard-ipc-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ vi.mock('node:fs/promises', () => ({
stat: fsStatMock,
realpath: vi.fn(), // unused here; only satisfies filesystem-path-containment's named import
writeFile: fsWriteFileMock,
default: {
writeFile: fsWriteFileMock
}
default: { writeFile: fsWriteFileMock, mkdir: fsMkdirMock }
}))

vi.mock('../ipc/filesystem-auth', () => ({
Expand Down
7 changes: 7 additions & 0 deletions src/main/window/clipboard-ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { readClipboardCopiedFilePaths } from './clipboard-copied-file-paths'
import { buildClipboardImageThumbnail } from './clipboard-image-thumbnail'
import { writeClipboardTextAndVerify } from './clipboard-text-write-verify'
import { isDashboardPopoutRenderer } from './dashboard-popout-window'
import { restoreNativeChatPastes, sweepExpiredNativeChatPastes } from './native-chat-paste-files'

let trustedClipboardRendererWebContentsId: number | null = null

Expand Down Expand Up @@ -101,8 +102,10 @@ export function registerClipboardHandlers(store: Store): void {
ipcMain.removeHandler('clipboard:readImageThumbnail')
ipcMain.removeHandler('clipboard:hasImage')
ipcMain.removeHandler('clipboard:readFilePaths')
ipcMain.removeHandler('clipboard:restoreNativeChatPastes')

void cleanupExpiredRemoteClipboardFiles()
void sweepExpiredNativeChatPastes()
scheduleLegacyRemoteClipboardFileCleanup()

ipcMain.handle('clipboard:readText', async (event, options?: ReadClipboardTextOptions) => {
Expand All @@ -116,6 +119,10 @@ export function registerClipboardHandlers(store: Store): void {
return assertClipboardTextWithinLimitWithYield(clipboard.readText('selection'), options)
}
)
ipcMain.handle('clipboard:restoreNativeChatPastes', (event, paths: unknown) => {
assertTrustedClipboardSender(event)
return restoreNativeChatPastes(paths)
})
// Why: an unanswered paste reads as a dropped paste, so the composer probes
// the clipboard in memory before the (slower) save lands.
ipcMain.handle('clipboard:readImageThumbnail', (event): ClipboardImageThumbnail | null => {
Expand Down
2 changes: 1 addition & 1 deletion src/main/window/clipboard-runtime-owned-ssh-paste.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ vi.mock('node:fs/promises', () => ({
stat: vi.fn(),
realpath: vi.fn(),
writeFile: fsWriteFileMock,
default: { writeFile: fsWriteFileMock }
default: { writeFile: fsWriteFileMock, mkdir: vi.fn() }
}))
vi.mock('../ipc/filesystem-auth', () => ({
PATH_ACCESS_DENIED_MESSAGE: 'denied',
Expand Down
Loading
Loading