mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +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>
223 lines
8.3 KiB
TypeScript
223 lines
8.3 KiB
TypeScript
import { win32 as pathWin32 } from 'node:path'
|
||
import { isWindowsGitBashShellPath } from '../git-bash'
|
||
import { parseWslPath, toLinuxPath, toWindowsWslPath } from '../wsl'
|
||
import {
|
||
buildWslInteractiveLoginShellCommand,
|
||
escapeWslShCommandForWindows,
|
||
quotePosixShell
|
||
} from '../../shared/wsl-login-shell-command'
|
||
import { ensureShellReadyWrappersAt } from './local-pty-shell-ready'
|
||
import {
|
||
encodePowerShellCommand,
|
||
getPowerShellOsc133Bootstrap
|
||
} from '../powershell-osc133-bootstrap'
|
||
|
||
const CMD_EXE_COMMAND_LINE_MAX_CHARS = 8191
|
||
const STARTUP_COMMAND_TEXT_MAX_CHARS = 6000
|
||
const POWERSHELL_ENCODED_COMMAND_ARG_MAX_CHARS = 28_000
|
||
const CMD_UTF8_SETUP_COMMAND = 'chcp 65001 > nul'
|
||
// Why: Git for Windows' bash inherits the ConPTY console's OEM code page
|
||
// (CP437), so a TUI that writes UTF-8 bytes straight to the console — agents
|
||
// like Claude Code use WriteFile, not WriteConsoleW — renders as mojibake
|
||
// (`❯` -> `Γ¥»`). Switch the console to UTF-8, then exec the normal interactive
|
||
// login shell; cmd.exe and PowerShell already do the equivalent. The `;` (not
|
||
// `&&`) keeps startup working even if chcp.com is missing.
|
||
const GIT_BASH_UTF8_LOGIN_COMMAND = 'chcp.com 65001 >/dev/null 2>&1; exec "$BASH" --login -i'
|
||
|
||
/** Result of resolving a Windows shell to its launch args + effective cwd.
|
||
*
|
||
* Why this module exists: both the in-process LocalPtyProvider and the
|
||
* daemon-subprocess spawner must produce IDENTICAL launch args for the same
|
||
* (shellPath, cwd) pair. A prior drift let the daemon path always spawn
|
||
* PowerShell regardless of which shell the user picked — the renderer's
|
||
* shellOverride never reached the daemon's shell-args branches. Sharing the
|
||
* decision here keeps both paths honest. */
|
||
export type WindowsShellLaunchArgs = {
|
||
shellArgs: string[]
|
||
/** True when the startup command was embedded in shellArgs and must not be
|
||
* written again through stdin. */
|
||
startupCommandDeliveredInShellArgs?: boolean
|
||
/** The cwd node-pty should be spawned with. WSL cannot cd into a Windows
|
||
* path, so the wsl.exe branch returns the user's home as the effective cwd
|
||
* and injects `cd '<linux path>'` into shellArgs instead. */
|
||
effectiveCwd: string
|
||
/** The path the caller should still validate exists on disk. Equals cwd in
|
||
* every branch except wsl.exe (which validates the Windows cwd even though
|
||
* the shell itself launches from $HOME). */
|
||
validationCwd: string
|
||
}
|
||
|
||
export type WindowsShellWslContext = {
|
||
distro: string
|
||
treatPosixCwdAsWsl?: boolean
|
||
}
|
||
|
||
/**
|
||
* Returns a startup command that is safe to embed in cmd.exe launch args.
|
||
*
|
||
* Commands that could exceed Windows cmd.exe limits return null so callers
|
||
* keep the older stdin delivery path.
|
||
*/
|
||
function getCmdShellArgStartupCommand(command?: string): string | null {
|
||
if (!command || command.length > STARTUP_COMMAND_TEXT_MAX_CHARS) {
|
||
return null
|
||
}
|
||
// Why: node-pty's C-runtime argv escaping changes normal cmd quotes in `/K`
|
||
// delivery, while the interactive parser preserves them through stdin.
|
||
if (command.includes('"')) {
|
||
return null
|
||
}
|
||
const commandArg = `${CMD_UTF8_SETUP_COMMAND} & ${command}`
|
||
if (commandArg.length > CMD_EXE_COMMAND_LINE_MAX_CHARS) {
|
||
return null
|
||
}
|
||
return command
|
||
}
|
||
|
||
/**
|
||
* Builds the PowerShell -EncodedCommand payload for startup bootstrap.
|
||
*
|
||
* Short startup commands are appended to the bootstrap and marked as delivered;
|
||
* large payloads return the bootstrap alone so stdin delivery remains available.
|
||
*/
|
||
function getPowerShellEncodedCommand(startupCommand?: string): {
|
||
encodedCommand: string
|
||
startupCommandDeliveredInShellArgs?: boolean
|
||
} {
|
||
const bootstrap = getPowerShellOsc133Bootstrap()
|
||
if (!startupCommand || startupCommand.length > STARTUP_COMMAND_TEXT_MAX_CHARS) {
|
||
return { encodedCommand: encodePowerShellCommand(bootstrap) }
|
||
}
|
||
|
||
const command = `${bootstrap}\n${startupCommand}`
|
||
const encodedCommand = encodePowerShellCommand(command)
|
||
// Why: -EncodedCommand expands UTF-16 text into base64; keep a conservative
|
||
// margin under Windows CreateProcess' 32,767-character command line limit.
|
||
if (encodedCommand.length > POWERSHELL_ENCODED_COMMAND_ARG_MAX_CHARS) {
|
||
return { encodedCommand: encodePowerShellCommand(bootstrap) }
|
||
}
|
||
|
||
return {
|
||
encodedCommand,
|
||
startupCommandDeliveredInShellArgs: true
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Builds wsl.exe arguments that enter the target directory through the distro shell.
|
||
*/
|
||
function buildWslShellArgs(linuxCwd: string, distro?: string): string[] {
|
||
ensureShellReadyWrappersAt()
|
||
const setupCommand = [
|
||
`cd ${quotePosixShell(linuxCwd)}`,
|
||
'export PATH="$HOME/.local/bin:$PATH"',
|
||
buildWslInteractiveLoginShellCommand()
|
||
].join(' && ')
|
||
// Why: WSL users often customize zsh rather than bash; launch the distro's
|
||
// login shell so terminal PATH matches the environment Orca detects.
|
||
const shellArgs = ['--', 'sh', '-c', escapeWslShCommandForWindows(setupCommand)]
|
||
return distro ? ['-d', distro, ...shellArgs] : shellArgs
|
||
}
|
||
|
||
function normalizeMsysDrivePath(cwd: string): string {
|
||
const match = cwd.match(/^\/([A-Za-z])(?:\/(.*))?$/)
|
||
if (!match) {
|
||
return cwd
|
||
}
|
||
|
||
// Why: Git Bash/MSYS launch contexts can hand Windows terminals `/d/repo`;
|
||
// ConPTY requires the equivalent native drive path as its cwd.
|
||
const driveLetter = match[1].toUpperCase()
|
||
const rest = match[2]?.replace(/\//g, '\\') ?? ''
|
||
return rest ? `${driveLetter}:\\${rest}` : `${driveLetter}:\\`
|
||
}
|
||
|
||
/** Build the argv + effective cwd for a Windows shell launch.
|
||
*
|
||
* - cmd.exe: `/K chcp 65001 > nul` so multi-byte CJK output renders correctly.
|
||
* - powershell.exe / pwsh.exe: dot-source $PROFILE and force UTF-8 I/O so
|
||
* oh-my-posh / starship / PSReadLine keep working. `-NoExit` alone would
|
||
* skip the profile.
|
||
* - wsl.exe: translate the Windows cwd to /mnt/<drive>/... and enter the
|
||
* distro user's login shell.
|
||
* - anything else: no args, same cwd. */
|
||
export function resolveWindowsShellLaunchArgs(
|
||
shellPath: string,
|
||
cwd: string,
|
||
defaultCwd: string,
|
||
wslContext?: WindowsShellWslContext,
|
||
startupCommand?: string
|
||
): WindowsShellLaunchArgs {
|
||
const shellBasename = pathWin32.basename(shellPath).toLowerCase()
|
||
const nativeCwd = normalizeMsysDrivePath(cwd)
|
||
const isMsysDriveCwd = nativeCwd !== cwd
|
||
|
||
if (shellBasename === 'cmd.exe') {
|
||
const shellArgStartupCommand = getCmdShellArgStartupCommand(startupCommand)
|
||
return {
|
||
shellArgs: [
|
||
'/K',
|
||
shellArgStartupCommand
|
||
? `${CMD_UTF8_SETUP_COMMAND} & ${shellArgStartupCommand}`
|
||
: CMD_UTF8_SETUP_COMMAND
|
||
],
|
||
...(shellArgStartupCommand ? { startupCommandDeliveredInShellArgs: true } : {}),
|
||
effectiveCwd: nativeCwd,
|
||
validationCwd: nativeCwd
|
||
}
|
||
}
|
||
|
||
if (shellBasename === 'powershell.exe' || shellBasename === 'pwsh.exe') {
|
||
const powerShellCommand = getPowerShellEncodedCommand(startupCommand)
|
||
// Why: foreground-process status on Windows depends on OSC 133 C/D, and
|
||
// PowerShell needs a prompt/readline bootstrap after profiles finish.
|
||
return {
|
||
shellArgs: ['-NoLogo', '-NoExit', '-EncodedCommand', powerShellCommand.encodedCommand],
|
||
...(powerShellCommand.startupCommandDeliveredInShellArgs
|
||
? { startupCommandDeliveredInShellArgs: true }
|
||
: {}),
|
||
effectiveCwd: nativeCwd,
|
||
validationCwd: nativeCwd
|
||
}
|
||
}
|
||
|
||
if (isWindowsGitBashShellPath(shellPath)) {
|
||
return {
|
||
shellArgs: ['-c', GIT_BASH_UTF8_LOGIN_COMMAND],
|
||
effectiveCwd: nativeCwd,
|
||
validationCwd: nativeCwd
|
||
}
|
||
}
|
||
|
||
if (shellBasename === 'wsl.exe') {
|
||
const wslInfo = parseWslPath(cwd)
|
||
if (wslInfo) {
|
||
return {
|
||
shellArgs: buildWslShellArgs(wslInfo.linuxPath, wslInfo.distro),
|
||
effectiveCwd: defaultCwd,
|
||
validationCwd: cwd
|
||
}
|
||
}
|
||
if (wslContext?.treatPosixCwdAsWsl && cwd.startsWith('/') && !isMsysDriveCwd) {
|
||
return {
|
||
shellArgs: buildWslShellArgs(cwd, wslContext.distro),
|
||
effectiveCwd: defaultCwd,
|
||
validationCwd: toWindowsWslPath(cwd, wslContext.distro)
|
||
}
|
||
}
|
||
const driveMatch = nativeCwd.replace(/\\/g, '/').match(/^([A-Za-z]):\/?(.*)$/)
|
||
const linuxCwd = driveMatch ? toLinuxPath(nativeCwd) : '/mnt/c'
|
||
return {
|
||
shellArgs: buildWslShellArgs(linuxCwd, wslContext?.distro),
|
||
effectiveCwd: defaultCwd,
|
||
validationCwd: nativeCwd
|
||
}
|
||
}
|
||
|
||
return {
|
||
shellArgs: [],
|
||
effectiveCwd: nativeCwd,
|
||
validationCwd: nativeCwd
|
||
}
|
||
}
|