feat(github): add Codex/OpenCode model support for issue validation

- Support Codex and OpenCode models in issue validation
- Add reasoningEffort parameter for Codex model configuration
- Update validation logic to use structured output for Claude/Codex
- Update UI hooks and types for multi-provider model selection
This commit is contained in:
DhanushSantosh
2026-01-14 00:50:02 +05:30
parent fbb3f697e1
commit 253ab94646
8 changed files with 308 additions and 104 deletions

View File

@@ -5,6 +5,43 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
const GIT_REMOTE_ORIGIN_COMMAND = 'git remote get-url origin';
const GH_REPO_VIEW_COMMAND = 'gh repo view --json name,owner';
const GITHUB_REPO_URL_PREFIX = 'https://github.com/';
const GITHUB_HTTPS_REMOTE_REGEX = /https:\/\/github\.com\/([^/]+)\/([^/.]+)/;
const GITHUB_SSH_REMOTE_REGEX = /git@github\.com:([^/]+)\/([^/.]+)/;
interface GhRepoViewResponse {
name?: string;
owner?: {
login?: string;
};
}
async function resolveRepoFromGh(projectPath: string): Promise<{
owner: string;
repo: string;
} | null> {
try {
const { stdout } = await execAsync(GH_REPO_VIEW_COMMAND, {
cwd: projectPath,
env: execEnv,
});
const data = JSON.parse(stdout) as GhRepoViewResponse;
const owner = typeof data.owner?.login === 'string' ? data.owner.login : null;
const repo = typeof data.name === 'string' ? data.name : null;
if (!owner || !repo) {
return null;
}
return { owner, repo };
} catch {
return null;
}
}
export interface GitHubRemoteStatus { export interface GitHubRemoteStatus {
hasGitHubRemote: boolean; hasGitHubRemote: boolean;
remoteUrl: string | null; remoteUrl: string | null;
@@ -21,19 +58,38 @@ export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemo
}; };
try { try {
// Get the remote URL (origin by default) let remoteUrl = '';
const { stdout } = await execAsync('git remote get-url origin', { try {
cwd: projectPath, // Get the remote URL (origin by default)
env: execEnv, const { stdout } = await execAsync(GIT_REMOTE_ORIGIN_COMMAND, {
}); cwd: projectPath,
env: execEnv,
});
remoteUrl = stdout.trim();
status.remoteUrl = remoteUrl || null;
} catch {
// Ignore missing origin remote
}
const remoteUrl = stdout.trim(); const ghRepo = await resolveRepoFromGh(projectPath);
status.remoteUrl = remoteUrl; if (ghRepo) {
status.hasGitHubRemote = true;
status.owner = ghRepo.owner;
status.repo = ghRepo.repo;
if (!status.remoteUrl) {
status.remoteUrl = `${GITHUB_REPO_URL_PREFIX}${ghRepo.owner}/${ghRepo.repo}`;
}
return status;
}
// Check if it's a GitHub URL // Check if it's a GitHub URL
// Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git // Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/); if (!remoteUrl) {
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/); return status;
}
const httpsMatch = remoteUrl.match(GITHUB_HTTPS_REMOTE_REGEX);
const sshMatch = remoteUrl.match(GITHUB_SSH_REMOTE_REGEX);
const match = httpsMatch || sshMatch; const match = httpsMatch || sshMatch;
if (match) { if (match) {

View File

@@ -25,19 +25,24 @@ interface GraphQLComment {
updatedAt: string; updatedAt: string;
} }
interface GraphQLCommentConnection {
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: GraphQLComment[];
}
interface GraphQLIssueOrPullRequest {
__typename: 'Issue' | 'PullRequest';
comments: GraphQLCommentConnection;
}
interface GraphQLResponse { interface GraphQLResponse {
data?: { data?: {
repository?: { repository?: {
issue?: { issueOrPullRequest?: GraphQLIssueOrPullRequest | null;
comments: {
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: GraphQLComment[];
};
};
}; };
}; };
errors?: Array<{ message: string }>; errors?: Array<{ message: string }>;
@@ -45,6 +50,7 @@ interface GraphQLResponse {
/** Timeout for GitHub API requests in milliseconds */ /** Timeout for GitHub API requests in milliseconds */
const GITHUB_API_TIMEOUT_MS = 30000; const GITHUB_API_TIMEOUT_MS = 30000;
const COMMENTS_PAGE_SIZE = 50;
/** /**
* Validate cursor format (GraphQL cursors are typically base64 strings) * Validate cursor format (GraphQL cursors are typically base64 strings)
@@ -54,7 +60,7 @@ function isValidCursor(cursor: string): boolean {
} }
/** /**
* Fetch comments for a specific issue using GitHub GraphQL API * Fetch comments for a specific issue or pull request using GitHub GraphQL API
*/ */
async function fetchIssueComments( async function fetchIssueComments(
projectPath: string, projectPath: string,
@@ -70,24 +76,52 @@ async function fetchIssueComments(
// Use GraphQL variables instead of string interpolation for safety // Use GraphQL variables instead of string interpolation for safety
const query = ` const query = `
query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) { query GetIssueComments(
$owner: String!
$repo: String!
$issueNumber: Int!
$cursor: String
$pageSize: Int!
) {
repository(owner: $owner, name: $repo) { repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) { issueOrPullRequest(number: $issueNumber) {
comments(first: 50, after: $cursor) { __typename
totalCount ... on Issue {
pageInfo { comments(first: $pageSize, after: $cursor) {
hasNextPage totalCount
endCursor pageInfo {
} hasNextPage
nodes { endCursor
id }
author { nodes {
login id
avatarUrl author {
login
avatarUrl
}
body
createdAt
updatedAt
}
}
}
... on PullRequest {
comments(first: $pageSize, after: $cursor) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
author {
login
avatarUrl
}
body
createdAt
updatedAt
} }
body
createdAt
updatedAt
} }
} }
} }
@@ -99,6 +133,7 @@ async function fetchIssueComments(
repo, repo,
issueNumber, issueNumber,
cursor: cursor || null, cursor: cursor || null,
pageSize: COMMENTS_PAGE_SIZE,
}; };
const requestBody = JSON.stringify({ query, variables }); const requestBody = JSON.stringify({ query, variables });
@@ -140,10 +175,10 @@ async function fetchIssueComments(
throw new Error(response.errors[0].message); throw new Error(response.errors[0].message);
} }
const commentsData = response.data?.repository?.issue?.comments; const commentsData = response.data?.repository?.issueOrPullRequest?.comments;
if (!commentsData) { if (!commentsData) {
throw new Error('Issue not found or no comments data available'); throw new Error('Issue or pull request not found or no comments data available');
} }
const comments: GitHubComment[] = commentsData.nodes.map((node) => ({ const comments: GitHubComment[] = commentsData.nodes.map((node) => ({

View File

@@ -9,6 +9,17 @@ import { checkGitHubRemote } from './check-github-remote.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
const logger = createLogger('ListIssues'); const logger = createLogger('ListIssues');
const OPEN_ISSUES_LIMIT = 100;
const CLOSED_ISSUES_LIMIT = 50;
const ISSUE_LIST_FIELDS = 'number,title,state,author,createdAt,labels,url,body,assignees';
const ISSUE_STATE_OPEN = 'open';
const ISSUE_STATE_CLOSED = 'closed';
const GH_ISSUE_LIST_COMMAND = 'gh issue list';
const GH_STATE_FLAG = '--state';
const GH_JSON_FLAG = '--json';
const GH_LIMIT_FLAG = '--limit';
const LINKED_PRS_BATCH_SIZE = 20;
const LINKED_PRS_TIMELINE_ITEMS = 10;
export interface GitHubLabel { export interface GitHubLabel {
name: string; name: string;
@@ -69,34 +80,68 @@ async function fetchLinkedPRs(
// Build GraphQL query for batch fetching linked PRs // Build GraphQL query for batch fetching linked PRs
// We fetch up to 20 issues at a time to avoid query limits // We fetch up to 20 issues at a time to avoid query limits
const batchSize = 20; for (let i = 0; i < issueNumbers.length; i += LINKED_PRS_BATCH_SIZE) {
for (let i = 0; i < issueNumbers.length; i += batchSize) { const batch = issueNumbers.slice(i, i + LINKED_PRS_BATCH_SIZE);
const batch = issueNumbers.slice(i, i + batchSize);
const issueQueries = batch const issueQueries = batch
.map( .map(
(num, idx) => ` (num, idx) => `
issue${idx}: issue(number: ${num}) { issue${idx}: issueOrPullRequest(number: ${num}) {
number ... on Issue {
timelineItems(first: 10, itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]) { number
nodes { timelineItems(
... on CrossReferencedEvent { first: ${LINKED_PRS_TIMELINE_ITEMS}
source { itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
... on PullRequest { ) {
number nodes {
title ... on CrossReferencedEvent {
state source {
url ... on PullRequest {
number
title
state
url
}
}
}
... on ConnectedEvent {
subject {
... on PullRequest {
number
title
state
url
}
} }
} }
} }
... on ConnectedEvent { }
subject { }
... on PullRequest { ... on PullRequest {
number number
title timelineItems(
state first: ${LINKED_PRS_TIMELINE_ITEMS}
url itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
) {
nodes {
... on CrossReferencedEvent {
source {
... on PullRequest {
number
title
state
url
}
}
}
... on ConnectedEvent {
subject {
... on PullRequest {
number
title
state
url
}
} }
} }
} }
@@ -213,16 +258,35 @@ export function createListIssuesHandler() {
} }
// Fetch open and closed issues in parallel (now including assignees) // Fetch open and closed issues in parallel (now including assignees)
const repoQualifier =
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
const [openResult, closedResult] = await Promise.all([ const [openResult, closedResult] = await Promise.all([
execAsync( execAsync(
'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body,assignees --limit 100', [
GH_ISSUE_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${ISSUE_STATE_OPEN}`,
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${OPEN_ISSUES_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{ {
cwd: projectPath, cwd: projectPath,
env: execEnv, env: execEnv,
} }
), ),
execAsync( execAsync(
'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body,assignees --limit 50', [
GH_ISSUE_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${ISSUE_STATE_CLOSED}`,
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${CLOSED_ISSUES_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{ {
cwd: projectPath, cwd: projectPath,
env: execEnv, env: execEnv,

View File

@@ -6,6 +6,17 @@ import type { Request, Response } from 'express';
import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
import { checkGitHubRemote } from './check-github-remote.js'; import { checkGitHubRemote } from './check-github-remote.js';
const OPEN_PRS_LIMIT = 100;
const MERGED_PRS_LIMIT = 50;
const PR_LIST_FIELDS =
'number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body';
const PR_STATE_OPEN = 'open';
const PR_STATE_MERGED = 'merged';
const GH_PR_LIST_COMMAND = 'gh pr list';
const GH_STATE_FLAG = '--state';
const GH_JSON_FLAG = '--json';
const GH_LIMIT_FLAG = '--limit';
export interface GitHubLabel { export interface GitHubLabel {
name: string; name: string;
color: string; color: string;
@@ -57,16 +68,36 @@ export function createListPRsHandler() {
return; return;
} }
const repoQualifier =
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
const [openResult, mergedResult] = await Promise.all([ const [openResult, mergedResult] = await Promise.all([
execAsync( execAsync(
'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100', [
GH_PR_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${PR_STATE_OPEN}`,
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${OPEN_PRS_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{ {
cwd: projectPath, cwd: projectPath,
env: execEnv, env: execEnv,
} }
), ),
execAsync( execAsync(
'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50', [
GH_PR_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${PR_STATE_MERGED}`,
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${MERGED_PRS_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{ {
cwd: projectPath, cwd: projectPath,
env: execEnv, env: execEnv,

View File

@@ -3,7 +3,7 @@
* *
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification. * Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
* Runs asynchronously and emits events for progress and completion. * Runs asynchronously and emits events for progress and completion.
* Supports both Claude models and Cursor models. * Supports Claude, Codex, Cursor, and OpenCode models.
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
@@ -11,13 +11,19 @@ import type { EventEmitter } from '../../../lib/events.js';
import type { import type {
IssueValidationResult, IssueValidationResult,
IssueValidationEvent, IssueValidationEvent,
ModelAlias, ModelId,
CursorModelId,
GitHubComment, GitHubComment,
LinkedPRInfo, LinkedPRInfo,
ThinkingLevel, ThinkingLevel,
ReasoningEffort,
} from '@automaker/types';
import {
DEFAULT_PHASE_MODELS,
isClaudeModel,
isCodexModel,
isCursorModel,
isOpencodeModel,
} from '@automaker/types'; } from '@automaker/types';
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver'; import { resolvePhaseModel } from '@automaker/model-resolver';
import { extractJson } from '../../../lib/json-extractor.js'; import { extractJson } from '../../../lib/json-extractor.js';
import { writeValidation } from '../../../lib/validation-storage.js'; import { writeValidation } from '../../../lib/validation-storage.js';
@@ -39,9 +45,6 @@ import {
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
/** Valid Claude model values for validation */
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
/** /**
* Request body for issue validation * Request body for issue validation
*/ */
@@ -51,10 +54,12 @@ interface ValidateIssueRequestBody {
issueTitle: string; issueTitle: string;
issueBody: string; issueBody: string;
issueLabels?: string[]; issueLabels?: string[];
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */ /** Model to use for validation (Claude alias or provider model ID) */
model?: ModelAlias | CursorModelId; model?: ModelId;
/** Thinking level for Claude models (ignored for Cursor models) */ /** Thinking level for Claude models (ignored for non-Claude models) */
thinkingLevel?: ThinkingLevel; thinkingLevel?: ThinkingLevel;
/** Reasoning effort for Codex models (ignored for non-Codex models) */
reasoningEffort?: ReasoningEffort;
/** Comments to include in validation analysis */ /** Comments to include in validation analysis */
comments?: GitHubComment[]; comments?: GitHubComment[];
/** Linked pull requests for this issue */ /** Linked pull requests for this issue */
@@ -66,7 +71,7 @@ interface ValidateIssueRequestBody {
* *
* Emits events for start, progress, complete, and error. * Emits events for start, progress, complete, and error.
* Stores result on completion. * Stores result on completion.
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing). * Supports Claude/Codex models (structured output) and Cursor/OpenCode models (JSON parsing).
*/ */
async function runValidation( async function runValidation(
projectPath: string, projectPath: string,
@@ -74,13 +79,14 @@ async function runValidation(
issueTitle: string, issueTitle: string,
issueBody: string, issueBody: string,
issueLabels: string[] | undefined, issueLabels: string[] | undefined,
model: ModelAlias | CursorModelId, model: ModelId,
events: EventEmitter, events: EventEmitter,
abortController: AbortController, abortController: AbortController,
settingsService?: SettingsService, settingsService?: SettingsService,
comments?: ValidationComment[], comments?: ValidationComment[],
linkedPRs?: ValidationLinkedPR[], linkedPRs?: ValidationLinkedPR[],
thinkingLevel?: ThinkingLevel thinkingLevel?: ThinkingLevel,
reasoningEffort?: ReasoningEffort
): Promise<void> { ): Promise<void> {
// Emit start event // Emit start event
const startEvent: IssueValidationEvent = { const startEvent: IssueValidationEvent = {
@@ -111,8 +117,8 @@ async function runValidation(
let responseText = ''; let responseText = '';
// Determine if we should use structured output (Claude supports it, Cursor doesn't) // Determine if we should use structured output (Claude/Codex support it, Cursor/OpenCode don't)
const useStructuredOutput = !isCursorModel(model); const useStructuredOutput = isClaudeModel(model) || isCodexModel(model);
// Build the final prompt - for Cursor, include system prompt and JSON schema instructions // Build the final prompt - for Cursor, include system prompt and JSON schema instructions
let finalPrompt = basePrompt; let finalPrompt = basePrompt;
@@ -138,14 +144,20 @@ ${basePrompt}`;
'[ValidateIssue]' '[ValidateIssue]'
); );
// Use thinkingLevel from request if provided, otherwise fall back to settings // Use request overrides if provided, otherwise fall back to settings
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel; let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel;
if (!effectiveThinkingLevel) { let effectiveReasoningEffort: ReasoningEffort | undefined = reasoningEffort;
if (!effectiveThinkingLevel || !effectiveReasoningEffort) {
const settings = await settingsService?.getGlobalSettings(); const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry = const phaseModelEntry =
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel; settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
const resolved = resolvePhaseModel(phaseModelEntry); const resolved = resolvePhaseModel(phaseModelEntry);
effectiveThinkingLevel = resolved.thinkingLevel; if (!effectiveThinkingLevel) {
effectiveThinkingLevel = resolved.thinkingLevel;
}
if (!effectiveReasoningEffort && typeof phaseModelEntry !== 'string') {
effectiveReasoningEffort = phaseModelEntry.reasoningEffort;
}
} }
logger.info(`Using model: ${model}`); logger.info(`Using model: ${model}`);
@@ -158,6 +170,7 @@ ${basePrompt}`;
systemPrompt: useStructuredOutput ? ISSUE_VALIDATION_SYSTEM_PROMPT : undefined, systemPrompt: useStructuredOutput ? ISSUE_VALIDATION_SYSTEM_PROMPT : undefined,
abortController, abortController,
thinkingLevel: effectiveThinkingLevel, thinkingLevel: effectiveThinkingLevel,
reasoningEffort: effectiveReasoningEffort,
readOnly: true, // Issue validation only reads code, doesn't write readOnly: true, // Issue validation only reads code, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
outputFormat: useStructuredOutput outputFormat: useStructuredOutput
@@ -262,6 +275,7 @@ export function createValidateIssueHandler(
issueLabels, issueLabels,
model = 'opus', model = 'opus',
thinkingLevel, thinkingLevel,
reasoningEffort,
comments: rawComments, comments: rawComments,
linkedPRs: rawLinkedPRs, linkedPRs: rawLinkedPRs,
} = req.body as ValidateIssueRequestBody; } = req.body as ValidateIssueRequestBody;
@@ -309,14 +323,17 @@ export function createValidateIssueHandler(
return; return;
} }
// Validate model parameter at runtime - accept Claude models or Cursor models // Validate model parameter at runtime - accept any supported provider model
const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias); const isValidModel =
const isValidCursorModel = isCursorModel(model); isClaudeModel(model) ||
isCursorModel(model) ||
isCodexModel(model) ||
isOpencodeModel(model);
if (!isValidClaudeModel && !isValidCursorModel) { if (!isValidModel) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`, error: 'Invalid model. Must be a Claude, Cursor, Codex, or OpenCode model ID (or alias).',
}); });
return; return;
} }
@@ -347,7 +364,8 @@ export function createValidateIssueHandler(
settingsService, settingsService,
validationComments, validationComments,
validationLinkedPRs, validationLinkedPRs,
thinkingLevel thinkingLevel,
reasoningEffort
) )
.catch(() => { .catch(() => {
// Error is already handled inside runValidation (event emitted) // Error is already handled inside runValidation (event emitted)

View File

@@ -9,7 +9,7 @@ import {
IssueValidationEvent, IssueValidationEvent,
StoredValidation, StoredValidation,
} from '@/lib/electron'; } from '@/lib/electron';
import type { LinkedPRInfo, PhaseModelEntry, ModelAlias, CursorModelId } from '@automaker/types'; import type { LinkedPRInfo, PhaseModelEntry, ModelId } from '@automaker/types';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { isValidationStale } from '../utils'; import { isValidationStale } from '../utils';
@@ -19,12 +19,10 @@ const logger = createLogger('IssueValidation');
/** /**
* Extract model string from PhaseModelEntry or string (handles both formats) * Extract model string from PhaseModelEntry or string (handles both formats)
*/ */
function extractModel( function extractModel(entry: PhaseModelEntry | string | undefined): ModelId | undefined {
entry: PhaseModelEntry | string | undefined
): ModelAlias | CursorModelId | undefined {
if (!entry) return undefined; if (!entry) return undefined;
if (typeof entry === 'string') { if (typeof entry === 'string') {
return entry as ModelAlias | CursorModelId; return entry as ModelId;
} }
return entry.model; return entry.model;
} }
@@ -228,8 +226,8 @@ export function useIssueValidation({
issue: GitHubIssue, issue: GitHubIssue,
options: { options: {
forceRevalidate?: boolean; forceRevalidate?: boolean;
model?: string | PhaseModelEntry; // Accept either string (backward compat) or PhaseModelEntry model?: ModelId | PhaseModelEntry; // Accept either model ID (backward compat) or PhaseModelEntry
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking level modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking/reasoning
comments?: GitHubComment[]; comments?: GitHubComment[];
linkedPRs?: LinkedPRInfo[]; linkedPRs?: LinkedPRInfo[];
} = {} } = {}
@@ -267,15 +265,16 @@ export function useIssueValidation({
? modelEntry ? modelEntry
: model : model
? typeof model === 'string' ? typeof model === 'string'
? { model: model as ModelAlias | CursorModelId } ? { model: model as ModelId }
: model : model
: phaseModels.validationModel; : phaseModels.validationModel;
const normalizedEntry = const normalizedEntry =
typeof effectiveModelEntry === 'string' typeof effectiveModelEntry === 'string'
? { model: effectiveModelEntry as ModelAlias | CursorModelId } ? { model: effectiveModelEntry as ModelId }
: effectiveModelEntry; : effectiveModelEntry;
const modelToUse = normalizedEntry.model; const modelToUse = normalizedEntry.model;
const thinkingLevelToUse = normalizedEntry.thinkingLevel; const thinkingLevelToUse = normalizedEntry.thinkingLevel;
const reasoningEffortToUse = normalizedEntry.reasoningEffort;
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
@@ -292,7 +291,8 @@ export function useIssueValidation({
currentProject.path, currentProject.path,
validationInput, validationInput,
modelToUse, modelToUse,
thinkingLevelToUse thinkingLevelToUse,
reasoningEffortToUse
); );
if (!result.success) { if (!result.success) {

View File

@@ -1,5 +1,5 @@
import type { GitHubIssue, StoredValidation, GitHubComment } from '@/lib/electron'; import type { GitHubIssue, StoredValidation, GitHubComment } from '@/lib/electron';
import type { ModelAlias, CursorModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types'; import type { ModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
export interface IssueRowProps { export interface IssueRowProps {
issue: GitHubIssue; issue: GitHubIssue;
@@ -37,7 +37,7 @@ export interface IssueDetailPanelProps {
/** Model override state */ /** Model override state */
modelOverride: { modelOverride: {
effectiveModelEntry: PhaseModelEntry; effectiveModelEntry: PhaseModelEntry;
effectiveModel: ModelAlias | CursorModelId; effectiveModel: ModelId;
isOverridden: boolean; isOverridden: boolean;
setOverride: (entry: PhaseModelEntry | null) => void; setOverride: (entry: PhaseModelEntry | null) => void;
}; };

View File

@@ -4,7 +4,7 @@
* Types for validating GitHub issues against the codebase using Claude SDK. * Types for validating GitHub issues against the codebase using Claude SDK.
*/ */
import type { ModelAlias } from './model.js'; import type { ModelId } from './model.js';
/** /**
* Verdict from issue validation * Verdict from issue validation
@@ -137,8 +137,8 @@ export type IssueValidationEvent =
issueTitle: string; issueTitle: string;
result: IssueValidationResult; result: IssueValidationResult;
projectPath: string; projectPath: string;
/** Model used for validation (opus, sonnet, haiku) */ /** Model used for validation */
model: ModelAlias; model: ModelId;
} }
| { | {
type: 'issue_validation_error'; type: 'issue_validation_error';
@@ -162,8 +162,8 @@ export interface StoredValidation {
issueTitle: string; issueTitle: string;
/** ISO timestamp when validation was performed */ /** ISO timestamp when validation was performed */
validatedAt: string; validatedAt: string;
/** Model used for validation (opus, sonnet, haiku) */ /** Model used for validation */
model: ModelAlias; model: ModelId;
/** The validation result */ /** The validation result */
result: IssueValidationResult; result: IssueValidationResult;
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */ /** ISO timestamp when user viewed this validation (undefined = not yet viewed) */