mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`RuntimeGitTarget` carried `connectionId?: string` and no host id, so `undefined` spelled three different answers at once — "runtime: host", "unresolved", and "genuinely local". Its sole resolver read `store.getRepo(worktree.repoId)?.connectionId` and never looked at `worktree.hostId`, which outranks every repo row, so one arbitrarily chosen row decided the execution host for 36 downstream dispatches. The target now carries `executionHostId: ExecutionHostId` (never null, never optional), resolved through the shared rule that landed with #17909/#17919 and dispatched through the host-keyed routes from #18296. Dispatch sites call `requireRuntimeGitProvider`, where `null` means exactly one thing: the host is `local` and the command runs here as free functions. Four answers that used to collapse into one: - `ssh:x` with a rival row on `ssh:y` — routes to x. Previously the first row won, which is the reproduced cross-host leak. - `local` with a surviving `connectionId` — a row contradicting itself; no SSH connection is handed out. - `runtime:<env>` — throws `ExecutionHostNotDispatchableError`. Its repo row's connection names a target in the *server's* namespace; dialling it here reaches a same-named target on this client. - rival rows disagreeing with no worktree host — `worktree_execution_host_unresolved`, matching the launch path rather than guessing a row. An unreachable SSH host still throws `SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE`; loss of contact is never evidence of locality (docs/reference/ssh-execution-boundary.md). `resolveWorktreeLaunchHost` keeps its exact signature and now delegates to `resolveWorktreeHostRouting`, the same resolution answering "which host is this on" rather than "what may this client dial" — the git target needs the first question because `local` and `runtime:` are two different non-SSH answers. No wire change: `RuntimeGitTarget` is main-process internal, and the SSH and local model-discovery host keys are byte-identical to before. `RuntimeFileTarget` has the same defect in ~30 filesystem dispatches and is deliberately left for a follow-up.
291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
import type { CommitMessageDraftContext } from '../../shared/commit-message-generation'
|
|
import { getCommitMessageModelDiscoveryHostKey } from '../../shared/commit-message-host-key'
|
|
import type { HostedReviewProvider } from '../../shared/hosted-review'
|
|
import { withLinkedIssueDraftContext } from '../../shared/source-control-ai-action-variables'
|
|
import type { TuiAgent } from '../../shared/tui-agent'
|
|
import { getStagedCommitContext } from '../git/status'
|
|
import { SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE } from '../providers/ssh-git-dispatch'
|
|
import { loadPullRequestLinkedIssue } from '../source-control/pull-request-linked-issue'
|
|
import { resolveHostedReviewBodyForGeneration } from '../source-control/pull-request-template'
|
|
import { prepareLocalCommitMessageAgentEnv } from '../text-generation/commit-message-agent-environment'
|
|
import {
|
|
cancelGenerateCommitMessageLocal,
|
|
cancelGeneratePullRequestFieldsLocal,
|
|
discoverCommitMessageModelsLocal,
|
|
discoverCommitMessageModelsRemote,
|
|
generateCommitMessageFromContext,
|
|
generatePullRequestFieldsFromContext,
|
|
resolveCommitMessageSettings,
|
|
type DiscoverCommitMessageModelsResult,
|
|
type GenerateCommitMessageResult,
|
|
type GeneratePullRequestFieldsResult
|
|
} from '../text-generation/commit-message-text-generation'
|
|
import { getPullRequestDraftContext } from '../text-generation/pull-request-context'
|
|
import {
|
|
localGitOptionsForTarget,
|
|
runtimeGitRouteForTarget,
|
|
type RuntimeGitCommandHost
|
|
} from './runtime-git-command-target'
|
|
import {
|
|
getRuntimeGitGenerationSettings,
|
|
linkedIssueForTarget,
|
|
linkedIssueMetaForTarget,
|
|
localAgentRuntimeTargetForTarget,
|
|
localTextGenerationTargetForTarget,
|
|
pullRequestDraftGitExec,
|
|
type RuntimeCommitMessageSettingsOverride
|
|
} from './runtime-git-generation-context'
|
|
|
|
export class RuntimeGitGenerationCommands {
|
|
constructor(private readonly host: RuntimeGitCommandHost) {}
|
|
|
|
async generateRuntimeCommitMessage(
|
|
worktreeSelector: string,
|
|
settingsOverride?: RuntimeCommitMessageSettingsOverride
|
|
): Promise<GenerateCommitMessageResult> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const route = runtimeGitRouteForTarget(target)
|
|
const discoveryHostKey =
|
|
settingsOverride?.commitMessageDiscoveryHostKey ??
|
|
getCommitMessageModelDiscoveryHostKey(route.kind === 'ssh' ? route.connectionId : null)
|
|
const resolvedSettings = settingsOverride?.sourceControlAiResolvedParams
|
|
? { ok: true as const, params: settingsOverride.sourceControlAiResolvedParams }
|
|
: resolveCommitMessageSettings(
|
|
getRuntimeGitGenerationSettings(
|
|
this.host.getRuntimeSettings(),
|
|
settingsOverride,
|
|
'commitMessage'
|
|
),
|
|
discoveryHostKey,
|
|
'commitMessage',
|
|
target.repo ?? null
|
|
)
|
|
if (!resolvedSettings.ok) {
|
|
return { success: false, error: resolvedSettings.error }
|
|
}
|
|
|
|
if (route.kind === 'ssh') {
|
|
const provider = route.provider
|
|
if (!provider) {
|
|
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
|
|
}
|
|
let context: CommitMessageDraftContext | null
|
|
try {
|
|
context = await provider.getStagedCommitContext(target.worktree.path)
|
|
} catch (error) {
|
|
console.error('[runtime-git] Failed to read remote staged commit context:', error)
|
|
return { success: false, error: 'Failed to read staged changes.' }
|
|
}
|
|
if (!context) {
|
|
return { success: false, error: 'No staged changes to summarize.' }
|
|
}
|
|
context = withLinkedIssueDraftContext(context, linkedIssueForTarget(this.host, target))
|
|
return generateCommitMessageFromContext(context, resolvedSettings.params, {
|
|
kind: 'remote',
|
|
cwd: target.worktree.path,
|
|
execute: (plan, cwd, timeoutMs, operation) =>
|
|
provider.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
|
|
missingBinaryLocation: 'remote PATH'
|
|
})
|
|
}
|
|
|
|
let context: CommitMessageDraftContext | null
|
|
try {
|
|
context = await getStagedCommitContext(target.worktree.path, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
} catch (error) {
|
|
console.error('[runtime-git] Failed to read staged commit context:', error)
|
|
return { success: false, error: 'Failed to read staged changes.' }
|
|
}
|
|
if (!context) {
|
|
return { success: false, error: 'No staged changes to summarize.' }
|
|
}
|
|
context = withLinkedIssueDraftContext(context, linkedIssueForTarget(this.host, target))
|
|
const localEnv = await prepareLocalCommitMessageAgentEnv(
|
|
resolvedSettings.params.agentId,
|
|
this.host.getCommitMessageAgentEnvironment?.(),
|
|
localAgentRuntimeTargetForTarget(target)
|
|
)
|
|
if (!localEnv.ok) {
|
|
return { success: false, error: localEnv.error }
|
|
}
|
|
return generateCommitMessageFromContext(
|
|
context,
|
|
resolvedSettings.params,
|
|
localTextGenerationTargetForTarget(target, localEnv.env)
|
|
)
|
|
}
|
|
|
|
async cancelRuntimeGenerateCommitMessage(worktreeSelector: string): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const route = runtimeGitRouteForTarget(target)
|
|
if (route.kind === 'ssh') {
|
|
// Cancelling an unreachable host is a no-op, not a local cancel: the local registry is keyed
|
|
// by path and would abort an unrelated generation running here for the same path.
|
|
await route.provider?.cancelGenerateCommitMessage(target.worktree.path, 'commit-message')
|
|
return { ok: true }
|
|
}
|
|
cancelGenerateCommitMessageLocal(target.worktree.path)
|
|
return { ok: true }
|
|
}
|
|
|
|
async generateRuntimePullRequestFields(
|
|
worktreeSelector: string,
|
|
input: {
|
|
base: string
|
|
title: string
|
|
body: string
|
|
draft: boolean
|
|
provider?: HostedReviewProvider
|
|
useTemplate?: boolean
|
|
},
|
|
settingsOverride?: RuntimeCommitMessageSettingsOverride
|
|
): Promise<GeneratePullRequestFieldsResult> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const route = runtimeGitRouteForTarget(target)
|
|
const discoveryHostKey =
|
|
settingsOverride?.commitMessageDiscoveryHostKey ??
|
|
getCommitMessageModelDiscoveryHostKey(route.kind === 'ssh' ? route.connectionId : null)
|
|
const resolvedSettings = settingsOverride?.sourceControlAiResolvedParams
|
|
? { ok: true as const, params: settingsOverride.sourceControlAiResolvedParams }
|
|
: resolveCommitMessageSettings(
|
|
getRuntimeGitGenerationSettings(
|
|
this.host.getRuntimeSettings(),
|
|
settingsOverride,
|
|
'pullRequest'
|
|
),
|
|
discoveryHostKey,
|
|
'pullRequest',
|
|
target.repo ?? null
|
|
)
|
|
if (!resolvedSettings.ok) {
|
|
return { success: false, error: resolvedSettings.error }
|
|
}
|
|
|
|
const provider = route.kind === 'ssh' ? route.provider : null
|
|
if (route.kind === 'ssh' && !provider) {
|
|
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
|
|
}
|
|
const issueMeta = linkedIssueMetaForTarget(this.host, target)
|
|
const linkedIssueDetailsPromise = loadPullRequestLinkedIssue({
|
|
meta: issueMeta,
|
|
provider: input.provider,
|
|
repoPath: target.worktree.path,
|
|
connectionId: route.kind === 'ssh' ? route.connectionId : undefined,
|
|
localGitOptions:
|
|
route.kind === 'ssh'
|
|
? {}
|
|
: {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
}
|
|
})
|
|
let context: Awaited<ReturnType<typeof getPullRequestDraftContext>>
|
|
try {
|
|
const currentBody = await resolveHostedReviewBodyForGeneration({
|
|
body: input.body,
|
|
repoPath: target.worktree.path,
|
|
connectionId: route.kind === 'ssh' ? route.connectionId : undefined,
|
|
provider: input.provider,
|
|
useTemplate: input.useTemplate
|
|
})
|
|
context = await getPullRequestDraftContext(pullRequestDraftGitExec(target, route), {
|
|
base: input.base,
|
|
currentTitle: input.title,
|
|
currentBody,
|
|
currentDraft: input.draft
|
|
})
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to prepare branch for PR details.'
|
|
}
|
|
}
|
|
if (!context) {
|
|
return { success: false, error: 'No branch changes to summarize.' }
|
|
}
|
|
const linkedIssueDetails = await linkedIssueDetailsPromise
|
|
context = {
|
|
...withLinkedIssueDraftContext(context, issueMeta?.linkedIssue),
|
|
...(input.provider ? { provider: input.provider } : {}),
|
|
...(linkedIssueDetails ? { linkedIssueDetails } : {})
|
|
}
|
|
|
|
if (route.kind === 'ssh') {
|
|
return generatePullRequestFieldsFromContext(context, resolvedSettings.params, {
|
|
kind: 'remote',
|
|
cwd: target.worktree.path,
|
|
execute: (plan, cwd, timeoutMs, operation) =>
|
|
provider!.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
|
|
missingBinaryLocation: 'remote PATH'
|
|
})
|
|
}
|
|
const localEnv = await prepareLocalCommitMessageAgentEnv(
|
|
resolvedSettings.params.agentId,
|
|
this.host.getCommitMessageAgentEnvironment?.(),
|
|
localAgentRuntimeTargetForTarget(target)
|
|
)
|
|
if (!localEnv.ok) {
|
|
return { success: false, error: localEnv.error }
|
|
}
|
|
return generatePullRequestFieldsFromContext(
|
|
context,
|
|
resolvedSettings.params,
|
|
localTextGenerationTargetForTarget(target, localEnv.env)
|
|
)
|
|
}
|
|
|
|
async cancelRuntimeGeneratePullRequestFields(worktreeSelector: string): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const route = runtimeGitRouteForTarget(target)
|
|
if (route.kind === 'ssh') {
|
|
await route.provider?.cancelGenerateCommitMessage(target.worktree.path, 'pull-request-fields')
|
|
return { ok: true }
|
|
}
|
|
cancelGeneratePullRequestFieldsLocal(target.worktree.path)
|
|
return { ok: true }
|
|
}
|
|
|
|
async discoverRuntimeCommitMessageModels(
|
|
worktreeSelector: string,
|
|
agentId: string,
|
|
settingsOverride?: Pick<RuntimeCommitMessageSettingsOverride, 'agentCmdOverrides'>
|
|
): Promise<DiscoverCommitMessageModelsResult> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const typedAgentId = agentId as TuiAgent
|
|
const agentCommandOverride =
|
|
settingsOverride?.agentCmdOverrides?.[typedAgentId] ??
|
|
this.host.getRuntimeSettings().agentCmdOverrides?.[typedAgentId]
|
|
const route = runtimeGitRouteForTarget(target)
|
|
if (route.kind === 'ssh') {
|
|
const provider = route.provider
|
|
if (!provider) {
|
|
return { success: false, error: `No git provider for connection "${route.connectionId}"` }
|
|
}
|
|
return discoverCommitMessageModelsRemote(
|
|
typedAgentId,
|
|
target.worktree.path,
|
|
(plan, cwd, timeoutMs) => provider.executeCommitMessagePlan(plan, cwd, timeoutMs),
|
|
agentCommandOverride
|
|
)
|
|
}
|
|
const localEnv = await prepareLocalCommitMessageAgentEnv(
|
|
typedAgentId,
|
|
this.host.getCommitMessageAgentEnvironment?.(),
|
|
localAgentRuntimeTargetForTarget(target)
|
|
)
|
|
if (!localEnv.ok) {
|
|
return { success: false, error: localEnv.error }
|
|
}
|
|
const localOptions = localGitOptionsForTarget(target)
|
|
return localOptions.wslDistro
|
|
? discoverCommitMessageModelsLocal(typedAgentId, localEnv.env, agentCommandOverride, {
|
|
cwd: target.worktree.path,
|
|
wslDistro: localOptions.wslDistro
|
|
})
|
|
: discoverCommitMessageModelsLocal(typedAgentId, localEnv.env, agentCommandOverride)
|
|
}
|
|
}
|