Files
orca/src/shared/source-control-ai-actions.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

206 lines
6.5 KiB
TypeScript

import { isCustomAgentId, type CustomAgentId } from './commit-message-agent-spec'
import { isTuiAgent } from './tui-agent-config'
import type { TuiAgent } from './tui-agent'
// Why: the variable registry lives in `./source-control-ai-action-variables` for max-lines
// headroom. It is deliberately not re-exported here — one import path per symbol keeps a
// grep of that module's consumers complete (the chip row is the one that must not diverge).
export type SourceControlTextActionId = 'commitMessage' | 'pullRequest' | 'branchName'
export type SourceControlLaunchActionId =
| 'fixCommitFailure'
| 'fixPushFailure'
| 'fixChecks'
| 'resolveConflicts'
| 'resolveComments'
export type SourceControlActionId = SourceControlTextActionId | SourceControlLaunchActionId
export type SourceControlActionRecipe = {
agentId?: TuiAgent | CustomAgentId | null
commandInputTemplate?: string
agentArgs?: string
}
export type SourceControlAiActionDefaults = Partial<
Record<SourceControlActionId, SourceControlActionRecipe>
>
export const SOURCE_CONTROL_TEXT_ACTION_IDS = [
'commitMessage',
'pullRequest',
'branchName'
] as const satisfies readonly SourceControlTextActionId[]
export const SOURCE_CONTROL_LAUNCH_ACTION_IDS = [
'fixCommitFailure',
'fixPushFailure',
'fixChecks',
'resolveConflicts',
'resolveComments'
] as const satisfies readonly SourceControlLaunchActionId[]
export const SOURCE_CONTROL_ACTION_IDS = [
...SOURCE_CONTROL_TEXT_ACTION_IDS,
...SOURCE_CONTROL_LAUNCH_ACTION_IDS
] as const satisfies readonly SourceControlActionId[]
export const SOURCE_CONTROL_TEXT_ACTION_LABELS: Record<SourceControlTextActionId, string> = {
commitMessage: 'Commit message',
pullRequest: 'Pull request details',
branchName: 'Branch name'
}
export const SOURCE_CONTROL_LAUNCH_ACTION_LABELS: Record<SourceControlLaunchActionId, string> = {
fixCommitFailure: 'Commit failure fixes',
fixPushFailure: 'Push failure fixes',
fixChecks: 'Broken checks fixes',
resolveConflicts: 'Conflict resolution',
resolveComments: 'Review comment resolution'
}
export const SOURCE_CONTROL_ACTION_LABELS: Record<SourceControlActionId, string> = {
...SOURCE_CONTROL_TEXT_ACTION_LABELS,
...SOURCE_CONTROL_LAUNCH_ACTION_LABELS
}
export const DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES: Record<
SourceControlActionId,
string
> = {
commitMessage: '{basePrompt}',
pullRequest: '{basePrompt}',
branchName: '{basePrompt}',
fixCommitFailure: '{basePrompt}',
fixPushFailure: '{basePrompt}',
fixChecks: '{basePrompt}',
resolveConflicts: '{basePrompt}',
resolveComments: '{basePrompt}'
}
const ACTION_ID_SET = new Set<string>(SOURCE_CONTROL_ACTION_IDS)
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isSafeRecordKey(key: string): boolean {
return key !== '' && key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
}
function isSourceControlActionId(value: string): value is SourceControlActionId {
return ACTION_ID_SET.has(value)
}
export function normalizeSourceControlActionRecipe(
value: unknown
): SourceControlActionRecipe | undefined {
if (!isRecord(value)) {
return undefined
}
const normalized: SourceControlActionRecipe = {}
const agentId = value.agentId
if (
agentId === null ||
isTuiAgent(agentId) ||
(typeof agentId === 'string' && isCustomAgentId(agentId))
) {
normalized.agentId = agentId
}
if (typeof value.commandInputTemplate === 'string') {
normalized.commandInputTemplate = value.commandInputTemplate
}
if (typeof value.agentArgs === 'string') {
normalized.agentArgs = value.agentArgs
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}
export function normalizeSourceControlAiActionDefaults(
value: unknown
): SourceControlAiActionDefaults | undefined {
if (!isRecord(value)) {
return undefined
}
const normalized: SourceControlAiActionDefaults = {}
for (const [key, item] of Object.entries(value)) {
if (!isSafeRecordKey(key) || !isSourceControlActionId(key)) {
continue
}
const defaultValue = normalizeSourceControlActionRecipe(item)
if (defaultValue) {
normalized[key] = defaultValue
}
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}
export function readSourceControlActionDefault(
defaults: SourceControlAiActionDefaults | null | undefined,
actionId: SourceControlActionId
): SourceControlActionRecipe {
const value = defaults?.[actionId]
return {
...(value?.agentId !== undefined ? { agentId: value.agentId } : {}),
...(typeof value?.commandInputTemplate === 'string'
? { commandInputTemplate: value.commandInputTemplate.trim() }
: {}),
...(typeof value?.agentArgs === 'string' ? { agentArgs: value.agentArgs.trim() } : {})
}
}
export function resolveSourceControlActionCommandTemplate(
defaults: SourceControlAiActionDefaults | null | undefined,
actionId: SourceControlActionId
): string {
const template = readSourceControlActionDefault(defaults, actionId).commandInputTemplate
return template !== undefined
? template
: DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES[actionId]
}
export function setSourceControlActionDefault(
defaults: SourceControlAiActionDefaults | null | undefined,
actionId: SourceControlActionId,
value: SourceControlActionRecipe
): SourceControlAiActionDefaults {
return {
...defaults,
[actionId]: {
...defaults?.[actionId],
...value
}
}
}
export function setSourceControlActionAgentDefault(
defaults: SourceControlAiActionDefaults | null | undefined,
actionId: SourceControlActionId,
agentId: TuiAgent | CustomAgentId | null
): SourceControlAiActionDefaults {
return setSourceControlActionDefault(defaults, actionId, { agentId })
}
export function renderSourceControlActionCommandTemplate(
template: string,
variables: Record<string, string | null | undefined>
): string {
return template.replace(
/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}|\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}/g,
(match, doubleName, singleName) => {
const name = (doubleName ?? singleName) as string
// Why: placeholder names may start with letters or underscores.
// Why: only own keys are real variables; inherited Object.prototype names
// (e.g. `constructor`) must stay visible instead of rendering their value.
if (!Object.hasOwn(variables, name)) {
return match
}
const value = variables[name]
return value === undefined || value === null ? match : value
}
)
}