mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 08:02:21 +00:00
* fix(native-chat): an unsent draft survives a reload or quit The composer's draft text, its editor document and settled image refs lived only in module memory, so a reload, quit or crash lost whatever was typed, including text a Stop had just given back. They are now saved per pane scope in localStorage: typing is debounced and flushed when the window hides or closes, text given back to the composer is saved at once, an emptied or sent draft is removed at once, and only the newest drafts within the composer caches' existing bound are kept. * fix(native-chat): save draft images outside the state updater * fix(native-chat): one store owns each unsent draft The draft's text and its image chips were saved by two writers that each rebuilt the stored record from what storage held, so a draft that once went over the size cap, or one refused write, lost its images on the next save. One store now holds each composer scope's whole draft in memory and writes storage from it: typing is coalesced, returned text, chips and clears land at once, a refused write stays pending for the next flush, and all drafts together keep to a budget so the outbox can still save a send. Closing a tab drops its panes' drafts. * test(native-chat): returned text is stored before its images arrive * fix(native-chat): keep returned text and drop launch-seed copies across reloads A message given back from the outbox could be larger than one draft was allowed to store, so the saved draft was deleted and the returned text was gone after a reload. One draft may now use the whole draft budget, which holds the largest message a send accepts. An adopted launch link is also parked in the agent's input line. Restoring it after a reload, with no seed left to replace that copy, typed it twice, so the untouched copy is shown but not saved. A composer reused for another pane now shows that pane's images, so adding one never saves the previous pane's over them. * test(native-chat): a plain 260k given-back message stays saved * fix(native-chat): saved drafts drop pasted images and die with their worktree A pasted image lives in a temp file Orca wrote, and the permission to show it is held in memory only, so after a restart a restored pasted chip showed a generic icon, and once the temp file was cleaned it looked fine but the send failed after the composer cleared. Pasted images now stay in the draft for the current run only; images the user attached from their own files are still saved. Removing a worktree now deletes its terminal tabs' drafts, as closing the tab does. A paste that finished late in a composer that had already been replaced wrote that composer's old images back over a newer send. Dropping a placeholder chip no longer writes the saved draft. The pane-change reload in the images hook is removed: the composer is keyed by pane, so a pane change always mounts a new one. * fix(native-chat): the composer renders the draft store instead of keeping a copy A composer that had been replaced, for example by a question prompt, could still finish an attach or a send later and write its own old copy of the draft over the store, so a sent message or image came back. The composer now reads text and images from the draft store and changes the draft as it is now: an added image or file reference joins what is there, and a send that settles after a round trip, or after its composer was replaced, clears the draft only if it is still what was sent. Only chips still being written and an IME composition in progress stay local to the composer. Closing a tab now deletes drafts by the exact tab id: a second chat for one session has an id that extends the first one's, so a prefix match deleted its drafts too. * fix(native-chat): keep images pasted during a command and on shown composers An image pasted while /compact or /clear was still on its way to the host vanished when the command was accepted: the clear that followed also wiped the chip still being saved. A command that settles now clears only the draft it sent, and an image pasted meanwhile stays. A composer on screen could lose its pasted images, and its unsaved launch text, once 128 other drafts pushed its record out of memory, because those are never stored. A draft that a composer is showing is no longer evicted. * fix(native-chat): a draft being written is never evicted from memory A write to a pane while every other cached draft was on screen could evict the record it had just written, losing it. Also adds the test that a send settling after its composer unmounted clears the saved draft, through the real send hook. * fix(native-chat): restored images Orca couldn't keep come back to attach again A pasted screenshot in an unsent draft vanished after a restart with no notice, and a restored image whose file had been deleted looked fine until the send failed. Both now come back as a placeholder chip that names the image and says it wasn't saved with the draft. Send stays disabled, and the send button says to attach the image again or remove it. Attaching a file with the same name takes the placeholder's place. Two browser tabs of the web client on one chat now follow each other: when one sends or edits the draft, the other drops its copy and shows the new one, unless it has an edit of its own not yet saved. * test(native-chat): no send while an image waits to be attached again * fix(native-chat): pasted images in a draft come back after a restart A pasted screenshot lived in the OS temp folder with a read permission held in memory, so a draft could only bring it back as a placeholder. Local pastes are now written to Orca's own native-chat-pastes folder under the app's user data, and a draft keeps them as real images. On restore the composer asks the main process to re-grant each one; main grants a read only when the file's real path, symlinks and junctions resolved, is inside that folder, and reports anything else as not kept, which becomes the placeholder. Pastes older than 30 days are deleted at startup, far past the host's 24 hour window for a resent message; the sweep never follows a link and never blocks startup. SSH pastes and older temp-folder pastes still come back as placeholders. * test(native-chat): the paste sweep test ages the link itself * fix(native-chat): type the paste-folder path helper with PlatformPath * fix(native-chat): type the paste-folder path helper from path.posix * fix(native-chat): placeholder chips say what happened and only a user attach replaces one A placeholder now shows the image's name and a short hint on the chip itself, "Attach again" for a file and "Not kept" for a pasted image, with the longer explanation in its tooltip. The copy no longer says the image wasn't saved: it says it couldn't be brought back, and for a pasted image it asks only for removal, since a new paste can't match it. The send button now says to remove the image to send. Only an image the user attaches (picking, dropping or pasting) takes the place of a placeholder with its file name. An image Stop gives back is added beside it, so a different file with the same name no longer hides the reminder. * fix(native-chat): only composer pastes use the paste folder, and restore grants only real paths Every local clipboard image save had moved into the paste folder, whose macOS path has a space, so a screenshot pasted into a terminal or sent to a terminal-backed agent no longer attached. Only a native-chat composer paste goes there now, through an optional field on the existing save call; terminal, editor and phone pastes stay in the system temp folder as before. An image path sent to an agent's input is escaped the way a dropped image is. The restore re-grant now grants only the file's real path, and only when both the real path and the path as written are inside the paste folder, which must not itself be a link. The sweep deletes only Orca's paste files and skips a linked folder. * fix(native-chat): a restored paste is readable by the path its draft stored Restore granted only a paste's real path, but the composer reads the preview by the path its draft stored, so with user data reached through a link the restored chip showed a generic icon. The stored spelling is granted too; it is already proven to sit inside the paste folder. A test also pins that a composer paste asks for the paste folder. * fix(native-chat): grant a paste's stored spelling only when it names the same file A grant also covers its path's own real path, so a stored spelling that is itself a link inside the paste folder could have granted an outside file while the path as restored reached a real paste. The stored spelling is now granted only when it resolves to the same file; otherwise only the real path is, and the preview falls back to the generic icon. * test(native-chat): check the outside file by its real path too * fix(native-chat): a structured chat's draft belongs to its conversation Drafts of a structured chat were saved under the pane (tab id plus a hash of the session), so the follow-up that keys them by conversation would have left every draft saved by this build invisible after an upgrade, never shown and never deleted. They are now keyed by the conversation (`agent-session:<sessionId>`) from the start: every composer showing the conversation shares one draft, Stop and a queued card's Edit give text back to it, closing the tab keeps it, and removing the worktree deletes it. Terminal-backed chats keep the pane key and lose their draft when the tab is closed. A send now leaves whatever was added since it was sent, text typed or composed and images attached meanwhile, and clears only what was sent; a draft replaced in the meantime is left alone. Lifted from #25207 (62ef8e7804): the conversation key, the composer's draftScopeKey, keep-on-close and delete-on-worktree-removal, and the leave-what-was-added send settle. * fix(native-chat): every path that drops a worktree's tabs deletes their drafts A worktree's chat drafts were deleted only by the removal's own renderer teardown, which looked them up from the worktree's tab lists after the removal round trip. The host announces the change before it replies, and the listing refresh that starts can purge those tab lists first, so the teardown found no tabs and the drafts stayed. The bulk purge now deletes the drafts of the tabs it drops, and the teardown deletes them before it closes any tab. * fix(native-chat): only a user's delete removes a workspace's chat drafts The listing purge is reconciliation, not a delete: re-pairing a server under the same id purges its worktrees' tabs, and the host then restores the same chats under the same draft keys, so deleting drafts there erased them. Removing a worktree, a project or a folder workspace now reads the draft keys of the workspace's open chats before the host round trip, and deletes them only once the host confirms. A listing refresh that drops the tabs during the round trip no longer hides them, and a refused delete keeps them. * feat(native-chat): drafts live in IndexedDB with no budget, and a refused save shows Unsent drafts shared localStorage with the send outbox, so they kept to a 128-draft, 1M-character budget and the oldest quietly did not come back. They now live in their own IndexedDB store, all loaded once at startup, with no budget and no eviction. - Startup waits up to 1.5 s for the load alongside the session read; a slower load still fills in every draft not edited meanwhile. - Each write commits explicitly; on pagehide, beforeunload and hiding, anything storage has not confirmed is also journaled synchronously to localStorage and replayed by the next run. - A BroadcastChannel replaces the storage event: another window's save is taken unless this window has a change of its own not yet saved. - A refused save is retried on the next flush and shown: a warning icon by Send and on the draft's image chips, only after a refusal. - Each draft records the workspace and host it was written in, so removing a worktree, project or folder workspace also deletes the drafts of chats whose tab was closed earlier. * fix(native-chat): drafts set from the store are never saved back, and early appends merge - The editor no longer saves a value the field sets from the store (a late-loaded or adopted draft): that echo made another window's draft a local change here, so a send in one web tab came back from the other. A whole draft set on the editor uses its saved document, keeping its skill chips. Equal documents no longer count as a change. - Text or images given back, or attached, before the startup load lands are added to the loaded draft instead of replacing it; only typing wins. - The unload journal leaves out drafts already refused, keeps to 256,000 characters, and drops a draft's entry once any window confirms a change to it, so a replay never brings back a draft sent since. - A failed load is retried three times with backoff, warned about once, then left. - Adopting another window's write keeps images already checked, so their chips don't flash. - A refused draft shows one icon, by Send, with plainer copy; the chip badge and its string are gone. - A comment that said drafts' bounds retire orphans is corrected. * test(native-chat): an early append the load already read is not added twice * test(native-chat): the store-value editor test waits for the editor and checks no document is saved * fix(native-chat): early appends are read-modify-writes, and the load can't be skipped - Before the load lands, an append (text or images given back, a paste) reads the stored draft and writes it back with the append in one storage change, so a load that failed and was retried can never lose the saved draft. On landing, only appends that load could not have read (made after it began reading, or never written) are applied again, by in-run order rather than by comparing clocks. - The editor saves a value set from the store when this window has a change of its own behind it, so a mention accepted next to a skill chip keeps the chip; another window's draft or a late load still isn't saved back. - A refused draft is journaled again; the 256,000-character cap bounds it. - The draft load starts before the startup chain, and the first write starts it in a window whose startup never did; startup still waits for it before the session's tabs mount. * fix(native-chat): text given back is durable before its source is deleted Draft writes reach IndexedDB asynchronously, but Stop's restore and a queued card's Edit delete their source (the outbox entry, the card) right after adding the text to the draft. A crash before the write committed lost it. An addition (text or images given back or attached) is now journaled to localStorage synchronously, as data, the moment it is made, and dropped once storage confirms a change at least as new. The next run replays it onto the stored draft only when storage doesn't already hold it, and only where the draft doesn't already end with that text or hold that image, so a crash either side of the commit gives the text back exactly once and resends nothing. Only earlier runs' entries are replayed. The append reports whether the addition is durable. Edit, when it isn't (the journal full or refused), deletes the card only once the draft is saved, and keeps it if storage refuses. Additions may grow the journal to 800,000 characters; whole drafts still stop at 256,000. * test(native-chat): a kept paste is readable by the composer preview through chat-image access, with no grant * fix(native-chat): a per-scope write confirm, and the paste rule as C2 has it - nativeChatComposerDraftWriteSettled(scopeKey) settles once every write of that scope issued before the call has: true only after IndexedDB completed each transaction (an append made before the load landed included), false when one was refused or failed, and never on a timeout. A change still waiting for its deferred write is issued first. - A queued card's Edit deletes the card only on true, and keeps it on false. - A restored paste is kept when its real path is a file inside Orca's paste folder, as C2 resolved it. The tests keep every containment case and now check that neither an outside file nor the file a stored spelling names becomes readable through the preview's chat-image access, which reads only image files and needs no grant. * test(native-chat): the per-scope confirm settles at the IndexedDB transaction's complete, and false on abort, for an append before the load * fix(native-chat): a journaled removal always fits, and a load replays only what preceded it - A removal is never capped in the journal (it is tiny, and it is what keeps a sent draft from coming back); only whole drafts count against their 256,000 characters, and additions keep their own 800,000. - A load replays the journal as it stood when the load began reading, so an addition a live window journals later, whose own write is ordered after that read, isn't replayed over its newer typing. - A lifecycle test no longer says the paste is re-granted. * fix(native-chat): a chat with a draft is never reused or taken over #24917's empty-chat check read the composer draft under the pane key, while this branch keeps a structured chat's draft under its conversation key, so a chat holding typed or given-back text, or only an image, read as empty: a new chat could reuse it and another request's text could go into it. It reads the conversation's draft now, as C2 adapted it (d4e469b697), with the tests moved to that key. While the startup load of saved drafts is still running, no chat reads as empty: one may hold a saved draft memory doesn't have yet. The cost is only that a new chat opens instead of reusing one during that moment. * fix(native-chat): a returned text counts as already there only as its own paragraph Replaying an addition after a crash skipped it whenever the draft merely ended with its text, so "go" given back onto "please go" was lost. It now counts only when the draft equals the text or ends with it after a blank line. A whitespace-only draft counts as empty when text is added, and an addition made before the startup load lands is made again on the loaded draft only where that draft doesn't already hold it. * fix(native-chat): a hand-back the draft already holds stays safe until that draft is saved After merging main's shared returned-text rule, a hand-back the draft already ends with changes nothing, so it wrote no backup entry yet reported durable. If the draft holding that text was not saved yet, a crash after the source copy was deleted lost it. Journal the addition whenever the draft is not confirmed saved. * fix(lint): import worktree removal names once in the delete failure toast Main's #25668 imported from shared/worktree/removal twice in this file, which fails the type-aware lint gate (no-duplicate-imports) for every branch that merges current main.
63 lines
2.9 KiB
TypeScript
63 lines
2.9 KiB
TypeScript
import type { ClipboardImageThumbnail } from '../../shared/clipboard-image'
|
|
import type { ReadClipboardTextOptions } from '../../shared/clipboard-text'
|
|
import type { NativeFileDropPayload } from '../../shared/native-file-drop'
|
|
import type {
|
|
RichMarkdownContextMenuCommandPayload,
|
|
RichMarkdownContextMenuTableTarget
|
|
} from '../../shared/rich-markdown-context-menu'
|
|
|
|
export type UiWindowApi = {
|
|
readClipboardText: (options?: ReadClipboardTextOptions) => Promise<string>
|
|
readSelectionClipboardText: (options?: ReadClipboardTextOptions) => Promise<string>
|
|
saveClipboardImageAsTempFile: (args?: {
|
|
connectionId?: string | null
|
|
runtimeEnvironmentId?: string | null
|
|
/** A native-chat composer paste, kept where its draft can bring it back. */
|
|
forNativeChatDraft?: boolean
|
|
}) => Promise<string | null>
|
|
clipboardHasImage: () => Promise<boolean | null>
|
|
/** Paths of files a file manager copied; empty when there are none or the host cannot list them. */
|
|
readClipboardFilePaths: () => Promise<string[]>
|
|
/** Which restored draft pastes are still kept: files really in Orca's paste folder. */
|
|
restoreNativeChatPastes: (
|
|
paths: string[]
|
|
) => Promise<{ path: string; kept: boolean; exists: boolean }[]>
|
|
readClipboardImageThumbnail: () => Promise<ClipboardImageThumbnail | null>
|
|
writeClipboardText: (text: string) => Promise<void>
|
|
writeTerminalClipboardText: (text: string) => Promise<void>
|
|
writeSelectionClipboardText: (text: string) => Promise<void>
|
|
writeClipboardImage: (dataUrl: string) => Promise<void>
|
|
performNativePaste: (options?: { mode?: 'paste' | 'paste-and-match-style' }) => void
|
|
performNativeSelectionAction: (action: 'copy' | 'select-all') => void
|
|
writeClipboardFile: (
|
|
args:
|
|
| {
|
|
filePath: string
|
|
connectionId?: string | null
|
|
}
|
|
| string
|
|
) => Promise<{ ok: boolean; reason?: string }>
|
|
onFileDrop: (callback: (data: NativeFileDropPayload) => void) => () => void
|
|
getZoomLevel: () => number
|
|
setZoomLevel: (level: number) => void
|
|
syncTrafficLights: (zoomFactor: number) => void
|
|
setMarkdownEditorFocused: (focused: boolean) => void
|
|
setRichMarkdownContextMenuTarget: (target: RichMarkdownContextMenuTableTarget | null) => void
|
|
setTerminalInputFocused: (focused: boolean) => void
|
|
setFloatingFocus: (state: { panelFocused: boolean; terminalFocused: boolean }) => void
|
|
setShortcutRecorderFocused: (focused: boolean) => void
|
|
onRichMarkdownContextCommand: (
|
|
callback: (payload: RichMarkdownContextMenuCommandPayload) => void
|
|
) => () => void
|
|
onFullscreenChanged: (callback: (isFullScreen: boolean) => void) => () => void
|
|
minimize: () => void
|
|
maximize: () => void
|
|
isMaximized: () => Promise<boolean>
|
|
onMaximizeChanged: (callback: (isMaximized: boolean) => void) => () => void
|
|
requestClose: () => void
|
|
popupMenu: () => void
|
|
onWindowCloseRequested: (callback: (data: { isQuitting: boolean }) => void) => () => void
|
|
confirmWindowClose: () => void
|
|
notifyWindowRevealed: () => void
|
|
}
|