Files
orca/src/main/providers/windows-shell-args.ts
T
JinjingandOrca a03a3dd51b Render png on mobile (#9087)
* 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>
2026-07-16 18:21:08 -07:00

223 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}