mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix(runtime): a phone reads and saves Markdown tabs on a headless server With no window to relay through, markdown.readTab and markdown.saveTab answered renderer_unavailable. Main now reads the tab's file (or its saved draft) from disk and saves with a version compare plus the disk write precondition, in the shapes the window's editor relay returns. With a window attached the relay is unchanged. * Headless Markdown save: keep the draft's disk baseline, honour read-only tabs, re-find the record to clear * Use the disk baseline only for a draft * ci: run the phone Markdown spec's orcad case in the orca serve mode-switch job * Headless Markdown: answer from disk only; a window's saved draft stays its view state Clearing a saved draft wrote workspace layout from main, which no layout command carries. Main now never shows or edits a draft; the desktop's restore check reports a disk change. * Phone Markdown: one path through main and the disk, window or not A paired server's workspace is read and saved on that server with files.read / files.writeIfUnchanged, so it answers from its own disk and keeps the precondition. The window relay (ui:mobileMarkdownRequest and the renderer bridge) is removed. * e2e: a phone saves a Markdown tab the desktop holds unsaved edits in The phone reads the disk with the tab marked dirty; its save lands on disk, and the desktop keeps its draft and shows the changed-on-disk banner. * Group the Markdown disk host's imports * Drop an unneeded store cast from the dirty-readers test * Phone Markdown: refuse a save on a server too old to check the disk; cover the untitled tab A server before #30605 has no files.writeIfUnchanged, so the save is refused with the existing update-required message rather than written unchecked. * Phone Markdown: refuse a file an SSH host holds outside the workspace; pin the untitled answer * Phone Markdown: untitled tabs stay read-only; older servers save like the desktop editor; a closed-source preview still reads - The desktop lists an untitled document's tab with isUntitled (optional, additive); main answers it read-only with unsupported_untitled and refuses its save before touching the disk. - A server without the files.write.disk-precondition capability gets the desktop editor's guarded save: compare from here, then a plain files.write, or a conflict. Every call of a save is pinned to the pairing it probed. - A preview whose source tab was closed has no saved record; it reads its own file, read-only. * e2e: read the untitled tab's answer without a cast * Phone Markdown: pin a save's pairing before its first read A save captures the server's pairing before it reads, and every call of the save, the read included, is pinned to it, so a re-pair between the read and the write is refused. * Test: with no window, a phone saves an existing untitled.md and never creates a deleted one --------- Co-authored-by: E2E Test <e2e@test.local>
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { getClipboardTextByteLength, isClipboardTextByteLengthOverLimit } from './clipboard-text'
|
|
import { clampUtf8TextPrefix } from './utf8-byte-limits'
|
|
|
|
export const MOBILE_MARKDOWN_EDIT_MAX_BYTES = 256 * 1024
|
|
/** Markdown preview budget; the file preview keeps its own. Above it a mobile read returns a
|
|
* UTF-8-boundary prefix marked `truncated`, never a refusal. Sized like the 2 MiB terminal
|
|
* snapshot; the relay splice frame cap is 8 MiB. */
|
|
export const MOBILE_MARKDOWN_READ_MAX_BYTES = 2 * 1024 * 1024
|
|
|
|
export type RuntimeMarkdownReadOnlyReason =
|
|
| 'unsupported_preview'
|
|
| 'unsupported_tab'
|
|
| 'unsupported_untitled'
|
|
| 'file_too_large'
|
|
|
|
export type RuntimeMarkdownReadTabResult = {
|
|
tabId: string
|
|
filePath: string
|
|
relativePath: string
|
|
content: string
|
|
isDirty: boolean
|
|
version: string
|
|
source: 'draft' | 'file'
|
|
editable: boolean
|
|
readOnlyReason?: RuntimeMarkdownReadOnlyReason
|
|
/** Present only when `content` is a prefix; older phones ignore both fields. */
|
|
truncated?: boolean
|
|
/** The full document's UTF-8 size, sent with `truncated`. */
|
|
byteLength?: number
|
|
}
|
|
|
|
export type RuntimeMarkdownSaveTabResult = {
|
|
tabId: string
|
|
version: string
|
|
isDirty: false
|
|
content: string
|
|
}
|
|
|
|
export function hashMarkdownContent(content: string): string {
|
|
let hash = 0xcbf29ce484222325n
|
|
for (let i = 0; i < content.length; i += 1) {
|
|
hash ^= BigInt(content.charCodeAt(i))
|
|
hash = BigInt.asUintN(64, hash * 0x100000001b3n)
|
|
}
|
|
return `content:${utf8ByteLength(content)}:${hash.toString(16).padStart(16, '0')}`
|
|
}
|
|
|
|
export function isMarkdownContentByteLengthOverLimit(content: string, maxBytes: number): boolean {
|
|
return isClipboardTextByteLengthOverLimit(content, maxBytes)
|
|
}
|
|
|
|
export function utf8ByteLength(content: string): number {
|
|
return getClipboardTextByteLength(content)
|
|
}
|
|
|
|
export function truncateMobileMarkdownRead(
|
|
content: string
|
|
):
|
|
| { content: string; truncated: false }
|
|
| { content: string; truncated: true; byteLength: number } {
|
|
const byteLength = utf8ByteLength(content)
|
|
if (byteLength <= MOBILE_MARKDOWN_READ_MAX_BYTES) {
|
|
return { content, truncated: false }
|
|
}
|
|
return {
|
|
content: clampUtf8TextPrefix(content, MOBILE_MARKDOWN_READ_MAX_BYTES),
|
|
truncated: true,
|
|
byteLength
|
|
}
|
|
}
|