Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion electron/aiTabMetadata/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ async function loadShellEnv(): Promise<ShellEnv> {
return {}
}

async function getProviderEnv(): Promise<NodeJS.ProcessEnv> {
export async function getProviderEnv(): Promise<NodeJS.ProcessEnv> {
providerEnvPromise ??= loadShellEnv().then((shellEnv) => ({
...process.env,
...withCommonProviderPathDirs(shellEnv),
Expand Down
99 changes: 64 additions & 35 deletions electron/quickPush/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import type {
QuickPushPlan,
QuickPushPullRequest,
} from '../../src/types/terminay'
import type { AiTabMetadataService } from '../aiTabMetadata/service'
import { getProviderEnv, type AiTabMetadataService } from '../aiTabMetadata/service'

const execFileAsync = promisify(execFile)

const MAX_BUFFER = 1024 * 1024 * 16
const MAX_DIFF_CHARS = 60_000
const MAX_UNTRACKED_TOTAL_CHARS = 20_000
const MAX_UNTRACKED_FILE_CHARS = 8_000
const MAX_FILE_CONTEXT_TOTAL_CHARS = 120_000
const MAX_FILE_CONTEXT_CHARS = 30_000
const GIT_TIMEOUT_MS = 30_000

type PorcelainEntry = {
Expand All @@ -34,7 +34,7 @@ type QuickPushContext = {
changedFiles: string[]
statusText: string
diffText: string
untrackedText: string
fileContextText: string
warnings: string[]
}

Expand Down Expand Up @@ -76,8 +76,10 @@ function looksBinary(buffer: Buffer): boolean {
}

async function runGit(args: string[], cwd: string): Promise<string> {
const env = await getProviderEnv()
const { stdout } = await execFileAsync('git', args, {
cwd,
env,
maxBuffer: MAX_BUFFER,
timeout: GIT_TIMEOUT_MS,
})
Expand Down Expand Up @@ -329,11 +331,57 @@ function buildPrompt(context: QuickPushContext, action: QuickPushAction): string
'=== git diff (tracked changes) ===',
context.diffText.trim() || '(no tracked diff)',
'',
'=== new (untracked) files ===',
context.untrackedText.trim() || '(none)',
'=== changed file contents ===',
context.fileContextText.trim() || '(no readable file contents)',
].join('\n')
}

async function buildChangedFileContext(
repoRoot: string,
entries: PorcelainEntry[],
warnings: string[],
): Promise<string> {
const sections: string[] = []
let remainingBudget = MAX_FILE_CONTEXT_TOTAL_CHARS

for (const entry of entries) {
if (remainingBudget <= 0) {
sections.push(`--- ${entry.path} (omitted: file context budget exhausted) ---`)
continue
}

const isDeleted = entry.x === 'D' || entry.y === 'D'
const absolute = path.join(repoRoot, entry.path)

try {
const stats = await stat(absolute)
if (!stats.isFile()) {
sections.push(`--- ${entry.path} (not a regular file, omitted) ---`)
continue
}

const buffer = await readFile(absolute)
if (looksBinary(buffer)) {
sections.push(`--- ${entry.path} (binary, omitted) ---`)
continue
}

const fileBudget = Math.min(MAX_FILE_CONTEXT_CHARS, remainingBudget)
const text = truncate(buffer.toString('utf8'), fileBudget)
remainingBudget -= text.length
sections.push(`--- ${entry.path} ---\n${text}`)
} catch {
if (isDeleted) {
sections.push(`--- ${entry.path} (deleted) ---`)
} else {
warnings.push(`Could not read changed file "${entry.path}".`)
}
}
}

return sections.join('\n\n')
}

async function gatherContext(cwd: string): Promise<QuickPushContext> {
const warnings: string[] = []

Expand Down Expand Up @@ -378,41 +426,15 @@ async function gatherContext(cwd: string): Promise<QuickPushContext> {
}
const diffText = truncate(diffParts.join('\n\n'), MAX_DIFF_CHARS)

const untrackedEntries = entries.filter((entry) => entry.x === '?' && entry.y === '?')
const untrackedSections: string[] = []
let untrackedBudget = MAX_UNTRACKED_TOTAL_CHARS
for (const entry of untrackedEntries) {
if (untrackedBudget <= 0) {
untrackedSections.push(`--- ${entry.path} (omitted: untracked context budget exhausted) ---`)
continue
}

const absolute = path.join(repoRoot, entry.path)
try {
const stats = await stat(absolute)
if (!stats.isFile()) {
continue
}
const buffer = await readFile(absolute)
if (looksBinary(buffer)) {
untrackedSections.push(`--- ${entry.path} (binary, omitted) ---`)
continue
}
const clipped = truncate(buffer.toString('utf8'), Math.min(MAX_UNTRACKED_FILE_CHARS, untrackedBudget))
untrackedBudget -= clipped.length
untrackedSections.push(`--- ${entry.path} ---\n${clipped}`)
} catch {
warnings.push(`Could not read untracked file "${entry.path}".`)
}
}
const fileContextText = await buildChangedFileContext(repoRoot, entries, warnings)

return {
repoRoot,
branch,
changedFiles,
statusText: statusTextRaw,
diffText,
untrackedText: untrackedSections.join('\n\n'),
fileContextText,
warnings,
}
}
Expand Down Expand Up @@ -466,8 +488,10 @@ export class QuickPushService {

const run = async (label: string, args: string[]): Promise<string> => {
try {
const env = await getProviderEnv()
const { stdout, stderr } = await execFileAsync('git', args, {
cwd: repoRoot,
env,
maxBuffer: MAX_BUFFER,
timeout: GIT_TIMEOUT_MS,
})
Expand Down Expand Up @@ -521,7 +545,12 @@ export class QuickPushService {
const { stdout, stderr } = await execFileAsync(
'gh',
['pr', 'create', '--title', pr.title, '--body', pr.body ?? ''],
{ cwd: repoRoot, maxBuffer: MAX_BUFFER, timeout: GIT_TIMEOUT_MS },
{
cwd: repoRoot,
env: await getProviderEnv(),
maxBuffer: MAX_BUFFER,
timeout: GIT_TIMEOUT_MS,
},
)
const output = `${stdout}${stderr}`.trim()
pullRequestUrl = extractUrl(`${stdout}\n${stderr}`)
Expand Down
Loading