mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
* Add mobile image-diff previews via shared data-URI builder - Extracts a `buildImageDataUri` helper (src/shared/image-data-uri.ts) shared by the desktop ImageViewer and mobile, so both trim whitespace-wrapped base64 and skip non-previewable mimes (e.g. application/pdf) the same way. - Adds mobile-diff-image-preview.ts to render binary git.diff results (add/modify/ delete) as images instead of falling back to "Binary preview unavailable". - Extracts resolveMobileFileTabDoc to consolidate the session file-tab loading logic (diff/image/html/text) out of the route file for testability. * Fix stale binary image fallback for empty modified diffs and relay reads - mobileDiffImageDataUri now distinguishes a true deletion (modified side absent) from a modify whose binary bytes arrived empty (relay/size-cap cases), returning null instead of the stale pre-change image - readWorkingDiffFile passes the file path to bufferToBlob so relay working-tree reads can detect previewable image extensions instead of always reporting empty binary content - add mobile-file-tab-doc.test.ts covering diff/image/binary/text resolution paths * Regenerate skill bundle manifest for 1.4.144-rc.2 Co-authored-by: Orca <help@stably.ai> * fix(review): trim comments to AGENTS.md's one/two-line why-only rule Comments in mobile-diff-image-preview.ts and mobile-file-tab-doc.ts ran 3-6 lines and narrated mechanism instead of stating only the non-obvious reason, per AGENTS.md's "Code Comments: Document the Why, Briefly" rule. Co-authored-by: Orca <help@stably.ai> * Distinguish read failures from true deletions in binary diff results - Working-tree stat/readFile errors and relay reads previously collapsed onto the same empty-content signal as a genuine deletion, letting previewers fall back to stale original bytes on a failed read. - Add modifiedDeleted/missing flags through status.ts, git-handler-ops, and git-working-file-read so only proven deletions trigger the original-bytes fallback; failed reads now return null. - Tighten buildImageDataUri to accept only image/* mimes instead of special-casing application/pdf. * fix(relay): expect missing:false on index blob maxBuffer overflow readBlobAtIndex now returns a missing flag so staged deletions are distinct from size-capped binary reads; update the overflow test. * Allow opening deleted files to show pre-delete text or image diffs Deleted files can now be opened to view their pre-delete content via git.diff (including images via modifiedDeleted). Only unresolved conflicts remain unopenable. Centralizes the canOpen rule in canOpenMobileGitStatusEntry() to keep opener guards consistent across the mobile source control UI. --------- Co-authored-by: Orca <help@stably.ai>
153 lines
4.1 KiB
TypeScript
153 lines
4.1 KiB
TypeScript
import {
|
|
ArrowDown,
|
|
ArrowDownUp,
|
|
ArrowUp,
|
|
Check,
|
|
CloudUpload,
|
|
GitBranch,
|
|
GitPullRequestArrow,
|
|
History,
|
|
RefreshCw,
|
|
type LucideIcon
|
|
} from 'lucide-react-native'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import type { MobileSourceControlActionIcon } from './mobile-source-control-actions'
|
|
import type { MobileDiffLine } from '../session/mobile-diff-lines'
|
|
import type { MobileHighlightedDiffLine } from '../session/mobile-file-syntax'
|
|
import type {
|
|
MobileGitBranchChangeEntry,
|
|
MobileGitBranchCompareResult,
|
|
MobileGitBranchCompareSummary
|
|
} from './mobile-branch-compare'
|
|
import {
|
|
canOpenMobileGitStatusEntry,
|
|
isMobileGitDiscardableEntry,
|
|
isMobileGitStageableEntry,
|
|
type MobileGitFileStatus,
|
|
type MobileGitStatusEntry,
|
|
type MobileGitStatusResult
|
|
} from './mobile-git-status'
|
|
|
|
export type ScreenState =
|
|
| { kind: 'loading' }
|
|
| { kind: 'ready'; status: MobileGitStatusResult }
|
|
| { kind: 'unavailable'; message: string }
|
|
| { kind: 'error'; message: string }
|
|
|
|
export type LoadStatusOptions = {
|
|
preserveReadyOnFailure?: boolean
|
|
clearActionErrorOnSuccess?: boolean
|
|
force?: boolean
|
|
}
|
|
|
|
export type StatusLoadInFlight = {
|
|
key: string
|
|
client: unknown
|
|
promise: Promise<boolean>
|
|
}
|
|
|
|
export type GitRequestError = Error & { code?: string }
|
|
export type GitCommitResult = { success: boolean; error?: string }
|
|
|
|
export type MobileGitStatusEntryView = MobileGitStatusEntry & {
|
|
canDiscard: boolean
|
|
canOpen: boolean
|
|
canStage: boolean
|
|
discardActionId: string
|
|
stageActionId: string
|
|
unstageActionId: string
|
|
}
|
|
|
|
// Decorate raw status entries with the row-level capability/action-id fields the
|
|
// file list needs. Opener guards must use the same canOpen rule.
|
|
export function buildMobileGitStatusEntryViews(
|
|
entries: readonly MobileGitStatusEntry[]
|
|
): MobileGitStatusEntryView[] {
|
|
return entries.map((entry) => ({
|
|
...entry,
|
|
canDiscard: isMobileGitDiscardableEntry(entry),
|
|
canOpen: canOpenMobileGitStatusEntry(entry),
|
|
canStage: isMobileGitStageableEntry(entry),
|
|
discardActionId: `discard:${entry.path}`,
|
|
stageActionId: `stage:${entry.path}`,
|
|
unstageActionId: `unstage:${entry.path}`
|
|
}))
|
|
}
|
|
|
|
export type MobileBranchCompareState =
|
|
| { kind: 'idle' }
|
|
| { kind: 'loading' }
|
|
| { kind: 'ready'; result: MobileGitBranchCompareResult }
|
|
| { kind: 'error'; message: string }
|
|
|
|
export type MobileBranchEntryView = MobileGitBranchChangeEntry & {
|
|
canOpen: boolean
|
|
}
|
|
|
|
export type MobileBranchDiffPreviewState =
|
|
| { kind: 'loading'; entry: MobileGitBranchChangeEntry }
|
|
| {
|
|
kind: 'ready'
|
|
entry: MobileGitBranchChangeEntry
|
|
summary: MobileGitBranchCompareSummary
|
|
lines: MobileHighlightedDiffLine<MobileDiffLine>[]
|
|
truncated: boolean
|
|
}
|
|
| { kind: 'error'; entry: MobileGitBranchChangeEntry; message: string }
|
|
|
|
export type GitDiffTextResult = {
|
|
kind: 'text'
|
|
originalContent: string
|
|
modifiedContent: string
|
|
}
|
|
|
|
export const KEYBOARD_COMMIT_BAR_CLEARANCE = 10
|
|
|
|
export const SOURCE_CONTROL_ACTION_ICONS: Record<MobileSourceControlActionIcon, LucideIcon> = {
|
|
commit: Check,
|
|
push: ArrowUp,
|
|
pull: ArrowDown,
|
|
sync: ArrowDownUp,
|
|
fetch: RefreshCw,
|
|
publish: CloudUpload,
|
|
rebase: GitBranch,
|
|
pr: GitPullRequestArrow,
|
|
branch: GitBranch,
|
|
history: History
|
|
}
|
|
|
|
export const SELECTOR_RETRY_COUNT = 3
|
|
export const SELECTOR_RETRY_DELAY_MS = 250
|
|
|
|
export function firstParam(value: string | string[] | undefined): string {
|
|
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
|
|
}
|
|
|
|
export function wait(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
export function formatBranchLabel(branch: string | undefined, head: string | undefined): string {
|
|
if (branch?.startsWith('refs/heads/')) {
|
|
return branch.slice('refs/heads/'.length)
|
|
}
|
|
return branch || head?.slice(0, 7) || 'No branch'
|
|
}
|
|
|
|
export function statusColor(status: MobileGitFileStatus): string {
|
|
switch (status) {
|
|
case 'added':
|
|
case 'copied':
|
|
return colors.statusGreen
|
|
case 'deleted':
|
|
return colors.statusRed
|
|
case 'renamed':
|
|
return colors.accentBlue
|
|
case 'untracked':
|
|
return colors.statusAmber
|
|
case 'modified':
|
|
default:
|
|
return colors.textSecondary
|
|
}
|
|
}
|