mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
* Unify dedicated agent launch choices and argument badges
* Enable action model and effort menus with argument migration
* Bound argument choice edit range processing
* Preserve independent action effort through launch and migration
* Preserve literal config arguments and explicit launch permissions
* Respect repository action argument groups and share launch controls
* Remove duplicate action launch command coverage
* Unify Source Control action choices and repository overrides
* Use precise types in action choice contracts
* Consolidate action choice test imports
* Correct action choice types and integration test boundaries
* Complete prepared terminal test result contract
* Align action integration expectations with canonical choices
* test(mobile): type the file preview retry test helpers so the test typecheck passes (#35431)
(cherry picked from commit 0774fc9373)
* Commit edited action arguments before planning and launch
Reconcile deliberate raw edits once into the submitted action recipe so replacement model and effort flags reach Save and Start together. Preserve captured sources and untouched load conflicts.
* Name portable Source Control dialog fixture results
308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
import type {
|
|
SourceControlActionLaunchPreferences,
|
|
SourceControlActionRecipe
|
|
} from './source-control-ai-actions'
|
|
import {
|
|
actionArgumentChoicesValue,
|
|
reconcileActionArgumentDocumentTransaction,
|
|
retireActionArgumentModelSelection,
|
|
demoteActionArgumentChoices,
|
|
readActionArgumentNode,
|
|
transportActionArgumentChoices
|
|
} from './source-control-action-argument-choices'
|
|
import {
|
|
actionModelSelectionChoices,
|
|
readActionModelSelection
|
|
} from './source-control-action-model-selection'
|
|
import { isTuiAgent } from './tui-agent-config'
|
|
import type { TuiAgent } from './tui-agent'
|
|
|
|
type CurrentActionSource = {
|
|
authoredAgentId: SourceControlActionRecipe['agentId']
|
|
authoredPreferences: SourceControlActionLaunchPreferences | undefined
|
|
}
|
|
|
|
export type CurrentActionSelection = {
|
|
current: Omit<
|
|
SourceControlActionRecipe,
|
|
'launchPreferences' | 'resolvedChoiceSources' | 'launchPreferencesYieldToAgentArgs'
|
|
>
|
|
source: CurrentActionSource &
|
|
(
|
|
| { scope: 'global' }
|
|
| {
|
|
scope: 'repo'
|
|
parentPreferences: SourceControlActionLaunchPreferences | undefined
|
|
ownsArgs: boolean
|
|
}
|
|
)
|
|
}
|
|
|
|
export type SelectedSourceControlActionRecipe = SourceControlActionRecipe & {
|
|
resolvedChoiceSources: NonNullable<SourceControlActionRecipe['resolvedChoiceSources']> & {
|
|
canonicalAgentId: TuiAgent | undefined
|
|
}
|
|
}
|
|
|
|
/** Canonical entries have one authored source and no inferred parent. */
|
|
export function canonicalSourceControlActionSelection(
|
|
recipe: SourceControlActionRecipe
|
|
): CurrentActionSelection {
|
|
const {
|
|
launchPreferences,
|
|
resolvedChoiceSources: _sources,
|
|
launchPreferencesYieldToAgentArgs: _yield,
|
|
...current
|
|
} = recipe
|
|
return {
|
|
current,
|
|
source: {
|
|
scope: 'global',
|
|
authoredAgentId: recipe.agentId,
|
|
authoredPreferences: launchPreferences
|
|
}
|
|
}
|
|
}
|
|
|
|
export function selectSourceControlActionRecipe(
|
|
selection: CurrentActionSelection
|
|
): SelectedSourceControlActionRecipe {
|
|
const { current, source } = selection
|
|
const own = source.authoredPreferences
|
|
const parent = source.scope === 'repo' ? source.parentPreferences : undefined
|
|
const ownsArgs = source.scope === 'repo' && source.ownsArgs
|
|
const ownModel = readActionModelSelection(own)
|
|
const parentModel = readActionModelSelection(parent)
|
|
const model =
|
|
ownsArgs || ownModel?.base === 'default'
|
|
? ownModel
|
|
: ownModel
|
|
? {
|
|
base: 'inherit' as const,
|
|
model: ownModel.model ?? parentModel?.model,
|
|
options: { ...parentModel?.options, ...ownModel.options }
|
|
}
|
|
: parentModel
|
|
const permission =
|
|
source.scope === 'repo'
|
|
? (own ?? (source.authoredAgentId === undefined ? parent : undefined))
|
|
: own
|
|
const preferences = source.scope === 'global' || ownsArgs ? own : (own ?? parent)
|
|
return {
|
|
...current,
|
|
...(preferences ? { launchPreferences: preferences } : {}),
|
|
resolvedChoiceSources: {
|
|
model: source.scope === 'global' ? ownModel : model,
|
|
permission,
|
|
canonicalAgentId: permission?.agentId ?? preferences?.agentId ?? parent?.agentId,
|
|
literalKeys:
|
|
ownsArgs || ownModel?.base !== 'default' || source.scope === 'global'
|
|
? (current.argumentChoices?.nodes ?? []).flatMap((node) => {
|
|
const key = readActionArgumentNode(
|
|
current.argumentChoices!.syntaxAgent,
|
|
node.text
|
|
)?.key
|
|
return key ? [key] : []
|
|
})
|
|
: []
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Read-only recipe adapters admit canonical data once; mutable invocations retain their inputs. */
|
|
export function sourceControlActionRecipeEntry(
|
|
recipe: SourceControlActionRecipe
|
|
): SelectedSourceControlActionRecipe {
|
|
return recipe.resolvedChoiceSources
|
|
? {
|
|
...recipe,
|
|
resolvedChoiceSources: {
|
|
...recipe.resolvedChoiceSources,
|
|
canonicalAgentId: recipe.resolvedChoiceSources.canonicalAgentId
|
|
}
|
|
}
|
|
: selectSourceControlActionRecipe(canonicalSourceControlActionSelection(recipe))
|
|
}
|
|
|
|
export function pickSourceControlActionAgent(
|
|
selection: CurrentActionSelection,
|
|
agent: TuiAgent,
|
|
observedAgent: TuiAgent | null,
|
|
surface: SourceControlActionRecipe['surface']
|
|
): CurrentActionSelection {
|
|
const actual = isTuiAgent(selection.current.agentId) ? selection.current.agentId : observedAgent
|
|
const same = actual === agent
|
|
const preferences = selection.source.authoredPreferences
|
|
const model = readActionModelSelection(preferences)
|
|
const alreadyBound =
|
|
preferences?.agentId === agent &&
|
|
(!model?.model || model.model.agentId === agent) &&
|
|
Object.values(model?.options ?? {}).every((entry) => entry.agentId === agent)
|
|
const document = selection.current.argumentChoices
|
|
const { surface: _surface, ...current } =
|
|
!same && document && document.syntaxAgent !== agent
|
|
? demoteActionArgumentChoices(selection.current)
|
|
: selection.current
|
|
return {
|
|
current: { ...current, agentId: agent, ...(surface ? { surface } : {}) },
|
|
source: same
|
|
? selection.source
|
|
: {
|
|
...selection.source,
|
|
authoredAgentId: agent,
|
|
authoredPreferences: alreadyBound ? preferences : undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
export function editSourceControlActionArguments(
|
|
selection: CurrentActionSelection,
|
|
agentArgs: string
|
|
): CurrentActionSelection {
|
|
return {
|
|
...selection,
|
|
current: {
|
|
...selection.current,
|
|
agentArgs,
|
|
argumentChoices: transportActionArgumentChoices(selection.current, agentArgs)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Finalize an explicit raw edit once, retaining captured sources beside the consumed view. */
|
|
export function commitSourceControlActionArguments(
|
|
selection: CurrentActionSelection,
|
|
agent: TuiAgent
|
|
): { selection: CurrentActionSelection; recipe: SelectedSourceControlActionRecipe } {
|
|
const selected = selectSourceControlActionRecipe(selection)
|
|
const { model, permission, literalKeys } = selected.resolvedChoiceSources
|
|
const document = selected.argumentChoices
|
|
const literal = actionArgumentChoicesValue({
|
|
argumentChoices:
|
|
document?.syntaxAgent === agent
|
|
? {
|
|
...document,
|
|
nodes: document.nodes.filter((node) => {
|
|
const key = readActionArgumentNode(document.syntaxAgent, node.text)!.key
|
|
const entry = key === 'model' ? model?.model : model?.options?.[key]
|
|
return literalKeys.includes(key) && !entry
|
|
})
|
|
}
|
|
: undefined
|
|
})
|
|
const typed = actionModelSelectionChoices(model, agent)
|
|
const current = selection.current
|
|
const transaction = reconcileActionArgumentDocumentTransaction(
|
|
{ ...current, agentId: agent },
|
|
'arguments-edit',
|
|
{
|
|
choices: {
|
|
...literal,
|
|
...typed,
|
|
options: { ...literal.options, ...typed.options },
|
|
...(permission?.agentId === agent ? { permissionMode: permission.permissionMode } : {})
|
|
},
|
|
literalKeys,
|
|
permissionEligible: !permission || permission.agentId === agent
|
|
}
|
|
)
|
|
const receipt = transaction.result
|
|
if (!receipt?.selected.length) {
|
|
return { selection, recipe: selected }
|
|
}
|
|
const touched = new Set(receipt.selected.map((entry) => entry.key))
|
|
const source = selection.source
|
|
const own = readActionModelSelection(source.authoredPreferences)
|
|
const ownsKey = (key: string) =>
|
|
source.scope === 'global' ||
|
|
source.ownsArgs ||
|
|
own?.base === 'default' ||
|
|
(key === 'model' ? own?.model : own?.options?.[key]) !== undefined
|
|
const update = (
|
|
preferences: SourceControlActionLaunchPreferences | undefined,
|
|
owns: (key: string) => boolean,
|
|
ownsPermission: boolean
|
|
) => {
|
|
const previous = readActionModelSelection(preferences)
|
|
const modelSelection = retireActionArgumentModelSelection(
|
|
previous,
|
|
agent,
|
|
touched,
|
|
receipt.choices,
|
|
'arguments-edit',
|
|
owns
|
|
)
|
|
if (modelSelection === previous && !(ownsPermission && touched.has('permissionMode'))) {
|
|
return preferences
|
|
}
|
|
const permissionMode =
|
|
ownsPermission && touched.has('permissionMode')
|
|
? receipt.choices.permissionMode
|
|
: preferences?.permissionMode
|
|
return modelSelection || permissionMode
|
|
? {
|
|
agentId: preferences?.agentId ?? agent,
|
|
...(modelSelection ? { modelSelection } : {}),
|
|
...(permissionMode ? { permissionMode } : {})
|
|
}
|
|
: undefined
|
|
}
|
|
const ownPermission =
|
|
source.scope === 'global' ||
|
|
source.authoredPreferences !== undefined ||
|
|
source.authoredAgentId !== undefined
|
|
const next: CurrentActionSelection = {
|
|
current: {
|
|
...current,
|
|
agentArgs: transaction.recipe.agentArgs,
|
|
argumentChoices: transaction.recipe.argumentChoices
|
|
},
|
|
source: {
|
|
...source,
|
|
authoredPreferences: update(source.authoredPreferences, ownsKey, ownPermission),
|
|
...(source.scope === 'repo'
|
|
? {
|
|
parentPreferences: update(
|
|
source.parentPreferences,
|
|
(key) => !ownsKey(key),
|
|
!ownPermission
|
|
)
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
const committedPermission = touched.has('permissionMode')
|
|
? {
|
|
agentId: permission?.agentId ?? agent,
|
|
permissionMode: receipt.choices.permissionMode
|
|
}
|
|
: permission
|
|
const nodes = transaction.recipe.argumentChoices
|
|
return {
|
|
selection: next,
|
|
recipe: {
|
|
...selected,
|
|
agentArgs: transaction.recipe.agentArgs,
|
|
argumentChoices: nodes,
|
|
resolvedChoiceSources: {
|
|
...selected.resolvedChoiceSources,
|
|
model: retireActionArgumentModelSelection(
|
|
model,
|
|
agent,
|
|
touched,
|
|
receipt.choices,
|
|
'arguments-edit'
|
|
),
|
|
permission: committedPermission,
|
|
canonicalAgentId: touched.has('permissionMode')
|
|
? committedPermission?.agentId
|
|
: selected.resolvedChoiceSources.canonicalAgentId,
|
|
literalKeys: (nodes?.nodes ?? []).flatMap((node) => {
|
|
const key = readActionArgumentNode(nodes!.syntaxAgent, node.text)!.key
|
|
return literalKeys.includes(key) || touched.has(key) ? [key] : []
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|