mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* WIP: Changes before auto-review fixes
* Customize Source Control AI action recipes
- Add per-action CLI arguments for generation and launch flows so saved
recipes can select model flags without putting prompts in argv.
- Share the text-generation dialog between commit messages and hosted-review
details, with first-run defaults and repo/global recipe support.
- Launch fix-check agents directly from saved recipes and harden prompt/template
handling for invalid args, blank prompts, and inherited variables.
* Allow custom commands for source control action recipes
- Let text action recipes save and resolve the custom-command sentinel
- Add settings UI for custom command recipes and preserve per-action defaults
- Split large source-control dialogs and direct launch helpers into focused modules
- Keep launch actions from treating custom text agents as runnable TUI agents
* Add per-repo Source Control AI enablement, custom command, and save-targ
- Repo overrides now support `enabled` and `customAgentCommand`, letting
repositories opt in/out of Source Control AI independently and supply a
repo-scoped custom command that takes precedence over the global one.
- Recipe-save dialogs gained a save-target selector ("Don't save / Save for
this repo / Save as global default") replacing the old boolean checkbox,
routing saves through the new `saveSourceControlActionRecipe` helper in
`source-control-ai-recipe-save.ts`.
- `normalizeRepoSourceControlAiOverrides` now returns `undefined` for empty
objects and passes the `null` sentinel through the IPC/RPC layer so the
persistence layer can clear repo overrides cleanly.
- `resolveSourceControlLaunchPlatform` resolves the correct shell platform
for SSH and WSL worktrees so agent launch commands are built correctly.
- Settings UI gained `RepositorySourceControlAiEnablement` and
`RepositorySourceControlAiCustomCommand` rows; draft/label logic was
extracted into focused modules to stay within lint line limits.
* Extract action recipe defaults into own component and use id-prefixed wo
- Move action recipe draft state and UI out of CommitMessageAiPane into SourceControlAiActionRecipeDefaults and source-control-ai-action-recipe-draft.ts to respect the max-lines lint rule
- Use toRuntimeWorktreeSelector() across all runtime git RPC calls so the runtime can resolve worktrees by ID rather than path
- Fix SSH launch platform resolution to use the repo's connection when the newly created worktree isn't hydrated yet
- Add edit and delete handlers for PR conversation comments with confirmation dialog
- Use text-status-success design token instead of hardcoded text-emerald-500
* add more search keyword
* Rename "Enable Source Control AI defaults" to "Show Source Control AI ac
* fix test
* Remove unused imports and variable assignment in launch-work-item-direct
* Fix test mocks to use `mocks.store` instead of `storeState.value` for di
* Extract Source Control AI logic into focused modules with fix-checks dia
---------
Co-authored-by: Orca <help@stably.ai>
187 lines
5.7 KiB
TypeScript
187 lines
5.7 KiB
TypeScript
import {
|
|
getCommitMessageAgentSpec,
|
|
getCommitMessageModel,
|
|
isCustomAgentId
|
|
} from './commit-message-agent-spec'
|
|
import { planCustomCommand, tokenizeCustomCommandTemplate } from './commit-message-prompt'
|
|
import type { TuiAgent } from './types'
|
|
|
|
// Why: planning is a pure transformation from "user request + prompt text"
|
|
// into "spawn-ready binary + argv". Keeping it in shared lets both the local
|
|
// generator (main process) and the SSH provider (which delegates to the
|
|
// relay over JSON-RPC) reuse the exact same validation and arg-building
|
|
// logic without duplicating the spec/model/thinking checks.
|
|
|
|
export type CommitMessagePlanInput = {
|
|
agentId: TuiAgent | 'custom'
|
|
model: string
|
|
thinkingLevel?: string
|
|
customAgentCommand?: string
|
|
agentCommandOverride?: string
|
|
agentArgs?: string
|
|
}
|
|
|
|
export type CommitMessagePlan = {
|
|
binary: string
|
|
args: string[]
|
|
/** Non-null when the prompt should be piped via stdin. */
|
|
stdinPayload: string | null
|
|
/** Human-readable label used in error prefixes (e.g. "Claude failed: ..."). */
|
|
label: string
|
|
}
|
|
|
|
export type CommitMessagePlanResult =
|
|
| { ok: true; plan: CommitMessagePlan }
|
|
| { ok: false; error: string }
|
|
|
|
export function planAgentBinary(
|
|
defaultBinary: string,
|
|
commandOverride: string | undefined
|
|
): { ok: true; binary: string; prefixArgs: string[] } | { ok: false; error: string } {
|
|
const command = commandOverride?.trim()
|
|
if (!command) {
|
|
return { ok: true, binary: defaultBinary, prefixArgs: [] }
|
|
}
|
|
|
|
const tokenized = tokenizeCustomCommandTemplate(command)
|
|
if (!tokenized.ok) {
|
|
return { ok: false, error: `Agent command override is invalid: ${tokenized.error}` }
|
|
}
|
|
const [binary, ...prefixArgs] = tokenized.tokens
|
|
if (!binary) {
|
|
return { ok: false, error: 'Agent command override must start with a binary name.' }
|
|
}
|
|
return { ok: true, binary, prefixArgs }
|
|
}
|
|
|
|
function planAdditionalAgentArgs(
|
|
agentArgs: string | null | undefined
|
|
): { ok: true; args: string[] } | { ok: false; error: string } {
|
|
const trimmed = agentArgs?.trim()
|
|
if (!trimmed) {
|
|
return { ok: true, args: [] }
|
|
}
|
|
const tokenized = tokenizeCustomCommandTemplate(trimmed)
|
|
if (!tokenized.ok) {
|
|
return { ok: false, error: `CLI arguments are invalid: ${tokenized.error}` }
|
|
}
|
|
return { ok: true, args: tokenized.tokens }
|
|
}
|
|
|
|
function insertAdditionalAgentArgs(args: {
|
|
baseArgs: string[]
|
|
agentArgs: string[]
|
|
promptDelivery: 'argv' | 'stdin'
|
|
prompt: string
|
|
}): string[] {
|
|
if (!args.agentArgs.length) {
|
|
return args.baseArgs
|
|
}
|
|
const promptPlaceholderIndex = args.baseArgs.lastIndexOf('{prompt}')
|
|
if (promptPlaceholderIndex !== -1) {
|
|
const merged = [...args.baseArgs]
|
|
merged.splice(promptPlaceholderIndex, 0, ...args.agentArgs)
|
|
return merged
|
|
}
|
|
if (
|
|
args.promptDelivery === 'argv' &&
|
|
args.prompt.length > 0 &&
|
|
args.baseArgs.at(-1) === args.prompt
|
|
) {
|
|
return [...args.baseArgs.slice(0, -1), ...args.agentArgs, args.prompt]
|
|
}
|
|
return [...args.baseArgs, ...args.agentArgs]
|
|
}
|
|
|
|
export function planCommitMessageGeneration(
|
|
input: CommitMessagePlanInput,
|
|
prompt: string
|
|
): CommitMessagePlanResult {
|
|
if (isCustomAgentId(input.agentId)) {
|
|
const command = input.customAgentCommand?.trim() ?? ''
|
|
if (!command) {
|
|
return {
|
|
ok: false,
|
|
error: 'Custom command is empty. Add one in Settings → Git → AI Commit Messages.'
|
|
}
|
|
}
|
|
const planned = planCustomCommand(command, prompt)
|
|
if (!planned.ok) {
|
|
return { ok: false, error: planned.error }
|
|
}
|
|
const agentArgs = planAdditionalAgentArgs(input.agentArgs)
|
|
if (!agentArgs.ok) {
|
|
return agentArgs
|
|
}
|
|
return {
|
|
ok: true,
|
|
plan: {
|
|
binary: planned.binary,
|
|
args: insertAdditionalAgentArgs({
|
|
baseArgs: planned.args,
|
|
agentArgs: agentArgs.args,
|
|
promptDelivery: planned.stdinPayload === null ? 'argv' : 'stdin',
|
|
prompt
|
|
}),
|
|
stdinPayload: planned.stdinPayload,
|
|
// Why: a custom command has no friendly name, so the binary doubles
|
|
// as the label in error prefixes ("ollama failed: ...").
|
|
label: planned.binary
|
|
}
|
|
}
|
|
}
|
|
|
|
const spec = getCommitMessageAgentSpec(input.agentId)
|
|
if (!spec) {
|
|
return { ok: false, error: `Agent "${input.agentId}" does not support AI commit messages.` }
|
|
}
|
|
const model = getCommitMessageModel(input.agentId, input.model)
|
|
if (!model) {
|
|
return { ok: false, error: `Model "${input.model}" is not available for ${spec.label}.` }
|
|
}
|
|
if (input.thinkingLevel) {
|
|
if (!model.thinkingLevels && spec.modelSource !== 'dynamic') {
|
|
return {
|
|
ok: false,
|
|
error: `Model "${model.label}" does not support a thinking effort level.`
|
|
}
|
|
}
|
|
if (model.thinkingLevels && !model.thinkingLevels.some((l) => l.id === input.thinkingLevel)) {
|
|
return {
|
|
ok: false,
|
|
error: `Thinking level "${input.thinkingLevel}" is not valid for ${model.label}.`
|
|
}
|
|
}
|
|
}
|
|
|
|
const argvPrompt = spec.promptDelivery === 'argv' ? prompt : ''
|
|
const baseArgs = spec.buildArgs({
|
|
prompt: argvPrompt,
|
|
model: input.model,
|
|
thinkingLevel: input.thinkingLevel
|
|
})
|
|
const agentArgs = planAdditionalAgentArgs(input.agentArgs)
|
|
if (!agentArgs.ok) {
|
|
return agentArgs
|
|
}
|
|
const args = insertAdditionalAgentArgs({
|
|
baseArgs,
|
|
agentArgs: agentArgs.args,
|
|
promptDelivery: spec.promptDelivery,
|
|
prompt: argvPrompt
|
|
})
|
|
const command = planAgentBinary(spec.binary, input.agentCommandOverride)
|
|
if (!command.ok) {
|
|
return { ok: false, error: command.error }
|
|
}
|
|
return {
|
|
ok: true,
|
|
plan: {
|
|
binary: command.binary,
|
|
args: [...command.prefixArgs, ...args],
|
|
stdinPayload: spec.promptDelivery === 'stdin' ? prompt : null,
|
|
label: spec.label
|
|
}
|
|
}
|
|
}
|