mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* fix(mobile): truncate oversize markdown reads instead of failing them The desktop bridge refused markdown over a private 512 KiB cap with file_too_large, which reached the phone as a generic runtime_error that the reader discarded, so a 632 KB file showed "Couldn't load markdown". Reads now return a UTF-8-boundary prefix under one shared 2 MiB budget, marked truncated with the full byteLength and read-only. The phone shows the truncation like file tabs do and maps refusal codes to real copy, so an older desktop's refusal reads "File too large for mobile preview". Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin that a truncated markdown read is never editable The read budget sits above the edit budget, so every truncated document is already read-only as file_too_large. Pin that ordering so a future budget change cannot make a prefix editable, and name the constant as the markdown preview budget, separate from the file preview's own. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refuse saves from a truncated markdown read A phone holding a truncated prefix must not write it back; the 256 KiB save guard refuses it as file_too_large before any version check. The shared budget test shrinks to the ordering it pins and names the bridge test as the behavioural pin. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): tighten markdown truncation types and measure once The read truncation measures the document once. The disk fallback drops its truncated-only read-only text, which the status line never showed, and both truncation fields are optional there. A markdown doc's flag is only ever true, and the schema comment names the hook, not line numbers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): drop a stale disk-fallback comment Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore: retrigger CI after #23675 landed on main Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
99 lines
2.7 KiB
TypeScript
99 lines
2.7 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 RuntimeMobileMarkdownRequest =
|
|
| {
|
|
id: string
|
|
operation: 'read'
|
|
worktreeId: string
|
|
tabId: string
|
|
}
|
|
| {
|
|
id: string
|
|
operation: 'save'
|
|
worktreeId: string
|
|
tabId: string
|
|
baseVersion: string
|
|
content: string
|
|
}
|
|
|
|
export type RuntimeMobileMarkdownResponse =
|
|
| {
|
|
id: string
|
|
ok: true
|
|
result: RuntimeMarkdownReadTabResult | RuntimeMarkdownSaveTabResult
|
|
}
|
|
| {
|
|
id: string
|
|
ok: false
|
|
error: string
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|