@@ -5,6 +5,22 @@ import { spawn } from 'node:child_process'
|
||||
|
||||
const delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
/** Map `fn` over `items` with at most `limit` running at once, preserving order.
|
||||
* Keeps the per-file git/disk reads overlapping without spawning hundreds of
|
||||
* subprocesses at once (macOS has a low default open-file limit). */
|
||||
async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
|
||||
const out = new Array<R>(items.length)
|
||||
let next = 0
|
||||
const worker = async (): Promise<void> => {
|
||||
while (next < items.length) {
|
||||
const i = next++
|
||||
out[i] = await fn(items[i], i)
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
|
||||
return out
|
||||
}
|
||||
|
||||
export type GitStatusLetter = 'A' | 'M' | 'D' | 'R' | 'U'
|
||||
|
||||
export interface GitChange {
|
||||
@@ -193,15 +209,19 @@ async function doLoad(root: string): Promise<GitLoad | null> {
|
||||
if (raw == null) return null
|
||||
|
||||
const { branch, files } = parseStatus(raw)
|
||||
const changes: GitChange[] = []
|
||||
for (const f of files) {
|
||||
// Each changed file needs its HEAD blob (a `git show` spawn) + its disk text.
|
||||
// Done serially this is O(files) subprocess spawns in a row — staging one file
|
||||
// re-reads ALL of them, which is the dominant cost of a reload. Run them with
|
||||
// bounded concurrency instead so the spawns overlap (cap keeps us well under
|
||||
// macOS's low default FD limit). Order is preserved by index.
|
||||
const changes = await mapLimit(files, 12, async (f) => {
|
||||
const { letter, staged } = classify(f.index, f.working)
|
||||
const isNew = f.index === '?' || f.index === 'A'
|
||||
const isDeleted = letter === 'D'
|
||||
const original = isNew ? '' : await headText(root, f.path)
|
||||
const updated = isDeleted ? '' : await diskText(root, f.path)
|
||||
changes.push({ path: f.path, status: letter, staged, original, updated })
|
||||
}
|
||||
return { path: f.path, status: letter, staged, original, updated } as GitChange
|
||||
})
|
||||
|
||||
return { branch, changes }
|
||||
}
|
||||
|
||||
@@ -30,11 +30,21 @@ let seq = 0
|
||||
* `npm_config_prefix` (e.g. "/opt/homebrew") into the child shell, which makes
|
||||
* nvm refuse to load ("nvm is not compatible with the npm_config_prefix
|
||||
* environment variable"). Strip it so the user's shell init runs cleanly.
|
||||
*
|
||||
* A macOS app launched from Spotlight/Finder (launchd GUI context) inherits NO
|
||||
* `LANG`/`LC_*`, so the child shell falls back to the `C`/POSIX locale — not
|
||||
* UTF-8. Anything multibyte the shell or `claude` emits then renders as high-byte
|
||||
* mojibake in xterm. Launching from a terminal (`npm run dev`) inherits the
|
||||
* terminal's UTF-8 locale, which is why dev looks fine and the packaged app does
|
||||
* not. Default a UTF-8 locale when none is set so both paths match.
|
||||
*/
|
||||
function ptyEnv(): { [key: string]: string } {
|
||||
const env = { ...process.env } as { [key: string]: string }
|
||||
delete env.npm_config_prefix
|
||||
delete env.npm_config_globalconfig
|
||||
if (process.platform !== 'win32' && !env.LC_ALL && !env.LC_CTYPE && !env.LANG) {
|
||||
env.LANG = 'en_US.UTF-8'
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user