mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: integrate thinking level support across various components
- Enhanced multiple server and UI components to include an optional thinking level parameter, improving the configurability of model interactions. - Updated request handlers and services to manage and pass the thinking level, ensuring consistent data handling across the application. - Refactored UI components to display and manage the selected model along with its thinking level, enhancing user experience and clarity. - Adjusted the Electron API and HTTP client to support the new thinking level parameter in requests, ensuring seamless integration. This update significantly improves the application's ability to adapt reasoning capabilities based on user-defined thinking levels, enhancing overall performance and user satisfaction.
This commit is contained in:
@@ -3,17 +3,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { ThinkingLevel } from '@automaker/types';
|
||||||
import { AgentService } from '../../../services/agent-service.js';
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createQueueAddHandler(agentService: AgentService) {
|
export function createQueueAddHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { sessionId, message, imagePaths, model } = req.body as {
|
const { sessionId, message, imagePaths, model, thinkingLevel } = req.body as {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
message: string;
|
message: string;
|
||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!sessionId || !message) {
|
if (!sessionId || !message) {
|
||||||
@@ -24,7 +26,12 @@ export function createQueueAddHandler(agentService: AgentService) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await agentService.addToQueue(sessionId, { message, imagePaths, model });
|
const result = await agentService.addToQueue(sessionId, {
|
||||||
|
message,
|
||||||
|
imagePaths,
|
||||||
|
model,
|
||||||
|
thinkingLevel,
|
||||||
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'Add to queue failed');
|
logError(error, 'Add to queue failed');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { Request, Response } from 'express';
|
|||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { resolveModelString } from '@automaker/model-resolver';
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
import { CLAUDE_MODEL_MAP, isCursorModel } from '@automaker/types';
|
import { CLAUDE_MODEL_MAP, isCursorModel, ThinkingLevel } from '@automaker/types';
|
||||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import type { SettingsService } from '../../../services/settings-service.js';
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
|
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
|
||||||
@@ -31,6 +31,8 @@ interface EnhanceRequestBody {
|
|||||||
enhancementMode: string;
|
enhancementMode: string;
|
||||||
/** Optional model override */
|
/** Optional model override */
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Optional thinking level for Claude models (ignored for Cursor models) */
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +130,8 @@ export function createEnhanceHandler(
|
|||||||
): (req: Request, res: Response) => Promise<void> {
|
): (req: Request, res: Response) => Promise<void> {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
|
const { originalText, enhancementMode, model, thinkingLevel } =
|
||||||
|
req.body as EnhanceRequestBody;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!originalText || typeof originalText !== 'string') {
|
if (!originalText || typeof originalText !== 'string') {
|
||||||
@@ -213,6 +216,7 @@ export function createEnhanceHandler(
|
|||||||
maxTurns: 1,
|
maxTurns: 1,
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
permissionMode: 'acceptEdits',
|
permissionMode: 'acceptEdits',
|
||||||
|
thinkingLevel: thinkingLevel, // Pass thinking level for Claude models
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
CursorModelId,
|
CursorModelId,
|
||||||
GitHubComment,
|
GitHubComment,
|
||||||
LinkedPRInfo,
|
LinkedPRInfo,
|
||||||
|
ThinkingLevel,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||||
import { resolvePhaseModel } from '@automaker/model-resolver';
|
import { resolvePhaseModel } from '@automaker/model-resolver';
|
||||||
@@ -54,6 +55,8 @@ interface ValidateIssueRequestBody {
|
|||||||
issueLabels?: string[];
|
issueLabels?: string[];
|
||||||
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
|
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
|
||||||
model?: ModelAlias | CursorModelId;
|
model?: ModelAlias | CursorModelId;
|
||||||
|
/** Thinking level for Claude models (ignored for Cursor models) */
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
/** 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 */
|
||||||
@@ -78,7 +81,8 @@ async function runValidation(
|
|||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
settingsService?: SettingsService,
|
settingsService?: SettingsService,
|
||||||
comments?: ValidationComment[],
|
comments?: ValidationComment[],
|
||||||
linkedPRs?: ValidationLinkedPR[]
|
linkedPRs?: ValidationLinkedPR[],
|
||||||
|
thinkingLevel?: ThinkingLevel
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Emit start event
|
// Emit start event
|
||||||
const startEvent: IssueValidationEvent = {
|
const startEvent: IssueValidationEvent = {
|
||||||
@@ -175,11 +179,15 @@ ${prompt}`;
|
|||||||
'[ValidateIssue]'
|
'[ValidateIssue]'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get thinkingLevel from phase model settings (the model comes from request, but thinkingLevel from settings)
|
// Use thinkingLevel from request if provided, otherwise fall back to settings
|
||||||
|
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel;
|
||||||
|
if (!effectiveThinkingLevel) {
|
||||||
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 { thinkingLevel } = resolvePhaseModel(phaseModelEntry);
|
const resolved = resolvePhaseModel(phaseModelEntry);
|
||||||
|
effectiveThinkingLevel = resolved.thinkingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
// Create SDK options with structured output and abort controller
|
// Create SDK options with structured output and abort controller
|
||||||
const options = createSuggestionsOptions({
|
const options = createSuggestionsOptions({
|
||||||
@@ -188,7 +196,7 @@ ${prompt}`;
|
|||||||
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||||
abortController,
|
abortController,
|
||||||
autoLoadClaudeMd,
|
autoLoadClaudeMd,
|
||||||
thinkingLevel,
|
thinkingLevel: effectiveThinkingLevel,
|
||||||
outputFormat: {
|
outputFormat: {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
schema: issueValidationSchema as Record<string, unknown>,
|
schema: issueValidationSchema as Record<string, unknown>,
|
||||||
@@ -308,6 +316,7 @@ export function createValidateIssueHandler(
|
|||||||
issueBody,
|
issueBody,
|
||||||
issueLabels,
|
issueLabels,
|
||||||
model = 'opus',
|
model = 'opus',
|
||||||
|
thinkingLevel,
|
||||||
comments: rawComments,
|
comments: rawComments,
|
||||||
linkedPRs: rawLinkedPRs,
|
linkedPRs: rawLinkedPRs,
|
||||||
} = req.body as ValidateIssueRequestBody;
|
} = req.body as ValidateIssueRequestBody;
|
||||||
@@ -392,7 +401,8 @@ export function createValidateIssueHandler(
|
|||||||
abortController,
|
abortController,
|
||||||
settingsService,
|
settingsService,
|
||||||
validationComments,
|
validationComments,
|
||||||
validationLinkedPRs
|
validationLinkedPRs,
|
||||||
|
thinkingLevel
|
||||||
)
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Error is already handled inside runValidation (event emitted)
|
// Error is already handled inside runValidation (event emitted)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ interface QueuedPrompt {
|
|||||||
message: string;
|
message: string;
|
||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,7 +638,12 @@ export class AgentService {
|
|||||||
*/
|
*/
|
||||||
async addToQueue(
|
async addToQueue(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
prompt: { message: string; imagePaths?: string[]; model?: string }
|
prompt: {
|
||||||
|
message: string;
|
||||||
|
imagePaths?: string[];
|
||||||
|
model?: string;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
}
|
||||||
): Promise<{ success: boolean; queuedPrompt?: QueuedPrompt; error?: string }> {
|
): Promise<{ success: boolean; queuedPrompt?: QueuedPrompt; error?: string }> {
|
||||||
const session = this.sessions.get(sessionId);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -649,6 +655,7 @@ export class AgentService {
|
|||||||
message: prompt.message,
|
message: prompt.message,
|
||||||
imagePaths: prompt.imagePaths,
|
imagePaths: prompt.imagePaths,
|
||||||
model: prompt.model,
|
model: prompt.model,
|
||||||
|
thinkingLevel: prompt.thinkingLevel,
|
||||||
addedAt: new Date().toISOString(),
|
addedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -778,6 +785,7 @@ export class AgentService {
|
|||||||
message: nextPrompt.message,
|
message: nextPrompt.message,
|
||||||
imagePaths: nextPrompt.imagePaths,
|
imagePaths: nextPrompt.imagePaths,
|
||||||
model: nextPrompt.model,
|
model: nextPrompt.model,
|
||||||
|
thinkingLevel: nextPrompt.thinkingLevel,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to process queued prompt:', error);
|
this.logger.error('Failed to process queued prompt:', error);
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
import { useState } from 'react';
|
import * as React from 'react';
|
||||||
import { Settings2, X, RotateCcw } from 'lucide-react';
|
import { Settings2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import type { ModelAlias, CursorModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
import type { ModelAlias, CursorModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||||
import { PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
import { PhaseModelSelector } from '@/components/views/settings-view/phase-models/phase-model-selector';
|
||||||
|
|
||||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract model string from PhaseModelEntry or string
|
* Normalize PhaseModelEntry or string to PhaseModelEntry
|
||||||
*/
|
*/
|
||||||
function extractModel(entry: PhaseModelEntry | string | null): ModelAlias | CursorModelId | null {
|
function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||||
if (!entry) return null;
|
|
||||||
if (typeof entry === 'string') {
|
if (typeof entry === 'string') {
|
||||||
return entry as ModelAlias | CursorModelId;
|
return { model: entry as ModelAlias | CursorModelId };
|
||||||
}
|
}
|
||||||
return entry.model;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelOverrideTriggerProps {
|
export interface ModelOverrideTriggerProps {
|
||||||
/** Current effective model (from global settings or explicit override) */
|
/** Current effective model entry (from global settings or explicit override) */
|
||||||
currentModel: ModelAlias | CursorModelId;
|
currentModelEntry: PhaseModelEntry;
|
||||||
/** Callback when user selects override */
|
/** Callback when user selects override */
|
||||||
onModelChange: (model: ModelAlias | CursorModelId | null) => void;
|
onModelChange: (entry: PhaseModelEntry | null) => void;
|
||||||
/** Optional: which phase this is for (shows global default) */
|
/** Optional: which phase this is for (shows global default) */
|
||||||
phase?: PhaseModelKey;
|
phase?: PhaseModelKey;
|
||||||
/** Size variants for different contexts */
|
/** Size variants for different contexts */
|
||||||
@@ -37,24 +34,8 @@ export interface ModelOverrideTriggerProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelLabel(modelId: ModelAlias | CursorModelId): string {
|
|
||||||
// Check Claude models
|
|
||||||
const claudeModel = CLAUDE_MODELS.find((m) => m.id === modelId);
|
|
||||||
if (claudeModel) return claudeModel.label;
|
|
||||||
|
|
||||||
// Check Cursor models (without cursor- prefix)
|
|
||||||
const cursorModel = CURSOR_MODELS.find((m) => m.id === `${PROVIDER_PREFIXES.cursor}${modelId}`);
|
|
||||||
if (cursorModel) return cursorModel.label;
|
|
||||||
|
|
||||||
// Check Cursor models (with cursor- prefix)
|
|
||||||
const cursorModelDirect = CURSOR_MODELS.find((m) => m.id === modelId);
|
|
||||||
if (cursorModelDirect) return cursorModelDirect.label;
|
|
||||||
|
|
||||||
return modelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModelOverrideTrigger({
|
export function ModelOverrideTrigger({
|
||||||
currentModel,
|
currentModelEntry,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
phase,
|
phase,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
@@ -62,29 +43,31 @@ export function ModelOverrideTrigger({
|
|||||||
isOverridden = false,
|
isOverridden = false,
|
||||||
className,
|
className,
|
||||||
}: ModelOverrideTriggerProps) {
|
}: ModelOverrideTriggerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const { phaseModels } = useAppStore();
|
||||||
const { phaseModels, enabledCursorModels } = useAppStore();
|
|
||||||
|
|
||||||
// Get the global default for this phase (extract model string from PhaseModelEntry)
|
const handleChange = (entry: PhaseModelEntry) => {
|
||||||
const globalDefault = phase ? extractModel(phaseModels[phase]) : null;
|
// If the new entry matches the global default, clear the override
|
||||||
|
// Otherwise, set it as override
|
||||||
|
if (phase) {
|
||||||
|
const globalDefault = phaseModels[phase];
|
||||||
|
const normalizedGlobal = normalizeEntry(globalDefault);
|
||||||
|
|
||||||
// Filter Cursor models to only show enabled ones
|
// Compare models (and thinking levels if both have them)
|
||||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
const modelsMatch = entry.model === normalizedGlobal.model;
|
||||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
const thinkingMatch =
|
||||||
return enabledCursorModels.includes(cursorId);
|
(entry.thinkingLevel || 'none') === (normalizedGlobal.thinkingLevel || 'none');
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelect = (model: ModelAlias | CursorModelId) => {
|
if (modelsMatch && thinkingMatch) {
|
||||||
onModelChange(model);
|
onModelChange(null); // Clear override
|
||||||
setOpen(false);
|
} else {
|
||||||
|
onModelChange(entry); // Set override
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onModelChange(entry);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
// Size classes for icon variant
|
||||||
onModelChange(null);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Size classes
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'h-6 w-6',
|
sm: 'h-6 w-6',
|
||||||
md: 'h-8 w-8',
|
md: 'h-8 w-8',
|
||||||
@@ -97,155 +80,47 @@ export function ModelOverrideTrigger({
|
|||||||
lg: 'w-5 h-5',
|
lg: 'w-5 h-5',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For icon variant, wrap PhaseModelSelector and hide text/chevron with CSS
|
||||||
|
if (variant === 'icon') {
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<div className={cn('relative inline-block', className)}>
|
||||||
<PopoverTrigger asChild>
|
<div className="relative [&_button>span]:hidden [&_button>svg:last-child]:hidden [&_button]:p-0 [&_button]:min-w-0 [&_button]:w-auto [&_button]:h-auto [&_button]:border-0 [&_button]:bg-transparent">
|
||||||
{variant === 'icon' ? (
|
<PhaseModelSelector
|
||||||
<button
|
value={currentModelEntry}
|
||||||
className={cn(
|
onChange={handleChange}
|
||||||
'relative rounded-md flex items-center justify-center',
|
compact
|
||||||
|
triggerClassName={cn(
|
||||||
|
'relative rounded-md',
|
||||||
'transition-colors duration-150',
|
'transition-colors duration-150',
|
||||||
'text-muted-foreground hover:text-foreground',
|
'text-muted-foreground hover:text-foreground',
|
||||||
'hover:bg-accent/50',
|
'hover:bg-accent/50',
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
title={`Model: ${getModelLabel(currentModel)}${isOverridden ? ' (overridden)' : ''}`}
|
disabled={false}
|
||||||
>
|
align="end"
|
||||||
<Settings2 className={iconSizes[size]} />
|
/>
|
||||||
|
</div>
|
||||||
{isOverridden && (
|
{isOverridden && (
|
||||||
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-brand-500 rounded-full" />
|
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-brand-500 rounded-full z-10 pointer-events-none" />
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : variant === 'button' ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size={size === 'md' ? 'default' : size}
|
|
||||||
className={cn('gap-2', className)}
|
|
||||||
>
|
|
||||||
<Settings2 className={iconSizes[size]} />
|
|
||||||
<span className="text-xs">{getModelLabel(currentModel)}</span>
|
|
||||||
{isOverridden && <div className="w-1.5 h-1.5 bg-brand-500 rounded-full" />}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1.5 text-xs',
|
|
||||||
'text-muted-foreground hover:text-foreground',
|
|
||||||
'transition-colors duration-150',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>Using {getModelLabel(currentModel)}</span>
|
|
||||||
<Settings2 className="w-3 h-3" />
|
|
||||||
{isOverridden && <div className="w-1.5 h-1.5 bg-brand-500 rounded-full" />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</PopoverTrigger>
|
|
||||||
|
|
||||||
<PopoverContent className="w-80 p-0" align="end">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-3 border-b border-border/50">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium">Model Override</h4>
|
|
||||||
{globalDefault && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Default: {getModelLabel(globalDefault)}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className="text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-3 space-y-4">
|
|
||||||
{/* Claude Models */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h5 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
||||||
Claude
|
|
||||||
</h5>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{CLAUDE_MODELS.map((model) => {
|
|
||||||
const isActive = currentModel === model.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={model.id}
|
|
||||||
onClick={() => handleSelect(model.id as ModelAlias)}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-2 rounded-lg text-xs font-medium text-center',
|
|
||||||
'transition-all duration-150',
|
|
||||||
isActive
|
|
||||||
? ['bg-brand-500/20 text-brand-500', 'border border-brand-500/40']
|
|
||||||
: [
|
|
||||||
'bg-accent/50 text-muted-foreground',
|
|
||||||
'border border-transparent',
|
|
||||||
'hover:bg-accent hover:text-foreground',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{model.label.replace('Claude ', '')}
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cursor Models */}
|
// For button and inline variants, use PhaseModelSelector in compact mode
|
||||||
{availableCursorModels.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h5 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
||||||
Cursor
|
|
||||||
</h5>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{availableCursorModels.slice(0, 6).map((model) => {
|
|
||||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
|
||||||
const isActive = currentModel === cursorId;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className={cn('relative', className)}>
|
||||||
key={model.id}
|
<PhaseModelSelector
|
||||||
onClick={() => handleSelect(cursorId)}
|
value={currentModelEntry}
|
||||||
className={cn(
|
onChange={handleChange}
|
||||||
'px-3 py-2 rounded-lg text-xs font-medium text-center truncate',
|
compact
|
||||||
'transition-all duration-150',
|
triggerClassName={variant === 'button' ? className : undefined}
|
||||||
isActive
|
disabled={false}
|
||||||
? ['bg-purple-500/20 text-purple-400', 'border border-purple-500/40']
|
/>
|
||||||
: [
|
|
||||||
'bg-accent/50 text-muted-foreground',
|
|
||||||
'border border-transparent',
|
|
||||||
'hover:bg-accent hover:text-foreground',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
title={model.description}
|
|
||||||
>
|
|
||||||
{model.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
{isOverridden && (
|
{isOverridden && (
|
||||||
<div className="p-3 border-t border-border/50">
|
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-brand-500 rounded-full z-10" />
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClear}
|
|
||||||
className="w-full gap-2 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-3.5 h-3.5" />
|
|
||||||
Use Global Default
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</div>
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,34 @@ export interface UseModelOverrideOptions {
|
|||||||
/** Which phase this override is for */
|
/** Which phase this override is for */
|
||||||
phase: PhaseModelKey;
|
phase: PhaseModelKey;
|
||||||
/** Initial override value (optional) */
|
/** Initial override value (optional) */
|
||||||
initialOverride?: ModelAlias | CursorModelId | null;
|
initialOverride?: PhaseModelEntry | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseModelOverrideResult {
|
export interface UseModelOverrideResult {
|
||||||
/** The effective model (override or global default) */
|
/** The effective model entry (override or global default) */
|
||||||
|
effectiveModelEntry: PhaseModelEntry;
|
||||||
|
/** The effective model string (for backward compatibility with APIs that only accept strings) */
|
||||||
effectiveModel: ModelAlias | CursorModelId;
|
effectiveModel: ModelAlias | CursorModelId;
|
||||||
/** Whether the model is currently overridden */
|
/** Whether the model is currently overridden */
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
/** Set a model override */
|
/** Set a model override */
|
||||||
setOverride: (model: ModelAlias | CursorModelId | null) => void;
|
setOverride: (entry: PhaseModelEntry | null) => void;
|
||||||
/** Clear the override and use global default */
|
/** Clear the override and use global default */
|
||||||
clearOverride: () => void;
|
clearOverride: () => void;
|
||||||
/** The global default for this phase */
|
/** The global default for this phase */
|
||||||
globalDefault: ModelAlias | CursorModelId;
|
globalDefault: PhaseModelEntry;
|
||||||
/** The current override value (null if not overridden) */
|
/** The current override value (null if not overridden) */
|
||||||
override: ModelAlias | CursorModelId | null;
|
override: PhaseModelEntry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize PhaseModelEntry or string to PhaseModelEntry
|
||||||
|
*/
|
||||||
|
function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
return { model: entry as ModelAlias | CursorModelId };
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,18 +50,18 @@ function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModel
|
|||||||
* Hook for managing model overrides per phase
|
* Hook for managing model overrides per phase
|
||||||
*
|
*
|
||||||
* Provides a simple way to allow users to override the global phase model
|
* Provides a simple way to allow users to override the global phase model
|
||||||
* for a specific run or context.
|
* for a specific run or context. Now supports PhaseModelEntry with thinking levels.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* function EnhanceDialog() {
|
* function EnhanceDialog() {
|
||||||
* const { effectiveModel, isOverridden, setOverride, clearOverride } = useModelOverride({
|
* const { effectiveModelEntry, isOverridden, setOverride, clearOverride } = useModelOverride({
|
||||||
* phase: 'enhancementModel',
|
* phase: 'enhancementModel',
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* return (
|
* return (
|
||||||
* <ModelOverrideTrigger
|
* <ModelOverrideTrigger
|
||||||
* currentModel={effectiveModel}
|
* currentModelEntry={effectiveModelEntry}
|
||||||
* onModelChange={setOverride}
|
* onModelChange={setOverride}
|
||||||
* phase="enhancementModel"
|
* phase="enhancementModel"
|
||||||
* isOverridden={isOverridden}
|
* isOverridden={isOverridden}
|
||||||
@@ -63,19 +75,25 @@ export function useModelOverride({
|
|||||||
initialOverride = null,
|
initialOverride = null,
|
||||||
}: UseModelOverrideOptions): UseModelOverrideResult {
|
}: UseModelOverrideOptions): UseModelOverrideResult {
|
||||||
const { phaseModels } = useAppStore();
|
const { phaseModels } = useAppStore();
|
||||||
const [override, setOverrideState] = useState<ModelAlias | CursorModelId | null>(initialOverride);
|
const [override, setOverrideState] = useState<PhaseModelEntry | null>(
|
||||||
|
initialOverride ? normalizeEntry(initialOverride) : null
|
||||||
|
);
|
||||||
|
|
||||||
// Extract model string from PhaseModelEntry (handles both old string format and new object format)
|
// Normalize global default to PhaseModelEntry
|
||||||
const globalDefault = extractModel(phaseModels[phase]);
|
const globalDefault = normalizeEntry(phaseModels[phase]);
|
||||||
|
|
||||||
const effectiveModel = useMemo(() => {
|
const effectiveModelEntry = useMemo(() => {
|
||||||
return override ?? globalDefault;
|
return override ?? globalDefault;
|
||||||
}, [override, globalDefault]);
|
}, [override, globalDefault]);
|
||||||
|
|
||||||
|
const effectiveModel = useMemo(() => {
|
||||||
|
return effectiveModelEntry.model;
|
||||||
|
}, [effectiveModelEntry]);
|
||||||
|
|
||||||
const isOverridden = override !== null;
|
const isOverridden = override !== null;
|
||||||
|
|
||||||
const setOverride = useCallback((model: ModelAlias | CursorModelId | null) => {
|
const setOverride = useCallback((entry: PhaseModelEntry | null) => {
|
||||||
setOverrideState(model);
|
setOverrideState(entry ? normalizeEntry(entry) : null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearOverride = useCallback(() => {
|
const clearOverride = useCallback(() => {
|
||||||
@@ -83,6 +101,7 @@ export function useModelOverride({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
effectiveModelEntry,
|
||||||
effectiveModel,
|
effectiveModel,
|
||||||
isOverridden,
|
isOverridden,
|
||||||
setOverride,
|
setOverride,
|
||||||
|
|||||||
@@ -323,7 +323,8 @@ export function AddFeatureDialog({
|
|||||||
const result = await api.enhancePrompt?.enhance(
|
const result = await api.enhancePrompt?.enhance(
|
||||||
newFeature.description,
|
newFeature.description,
|
||||||
enhancementMode,
|
enhancementMode,
|
||||||
enhancementOverride.effectiveModel
|
enhancementOverride.effectiveModel, // API accepts string, extract from PhaseModelEntry
|
||||||
|
enhancementOverride.effectiveModelEntry.thinkingLevel // Pass thinking level
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result?.success && result.enhancedText) {
|
if (result?.success && result.enhancedText) {
|
||||||
@@ -512,7 +513,7 @@ export function AddFeatureDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ModelOverrideTrigger
|
<ModelOverrideTrigger
|
||||||
currentModel={enhancementOverride.effectiveModel}
|
currentModelEntry={enhancementOverride.effectiveModelEntry}
|
||||||
onModelChange={enhancementOverride.setOverride}
|
onModelChange={enhancementOverride.setOverride}
|
||||||
phase="enhancementModel"
|
phase="enhancementModel"
|
||||||
isOverridden={enhancementOverride.isOverridden}
|
isOverridden={enhancementOverride.isOverridden}
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ import type {
|
|||||||
import { ModelOverrideTrigger } from '@/components/shared/model-override-trigger';
|
import { ModelOverrideTrigger } from '@/components/shared/model-override-trigger';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize PhaseModelEntry or string to PhaseModelEntry
|
||||||
|
*/
|
||||||
|
function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
return { model: entry as ModelAlias | CursorModelId };
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract model string from PhaseModelEntry or string
|
* Extract model string from PhaseModelEntry or string
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +81,7 @@ export function BacklogPlanDialog({
|
|||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [expandedChanges, setExpandedChanges] = useState<Set<number>>(new Set());
|
const [expandedChanges, setExpandedChanges] = useState<Set<number>>(new Set());
|
||||||
const [selectedChanges, setSelectedChanges] = useState<Set<number>>(new Set());
|
const [selectedChanges, setSelectedChanges] = useState<Set<number>>(new Set());
|
||||||
const [modelOverride, setModelOverride] = useState<ModelAlias | CursorModelId | null>(null);
|
const [modelOverride, setModelOverride] = useState<PhaseModelEntry | null>(null);
|
||||||
|
|
||||||
const { phaseModels } = useAppStore();
|
const { phaseModels } = useAppStore();
|
||||||
|
|
||||||
@@ -105,7 +115,8 @@ export function BacklogPlanDialog({
|
|||||||
setIsGeneratingPlan(true);
|
setIsGeneratingPlan(true);
|
||||||
|
|
||||||
// Use model override if set, otherwise use global default (extract model string from PhaseModelEntry)
|
// Use model override if set, otherwise use global default (extract model string from PhaseModelEntry)
|
||||||
const effectiveModel = modelOverride || extractModel(phaseModels.backlogPlanningModel);
|
const effectiveModelEntry = modelOverride || normalizeEntry(phaseModels.backlogPlanningModel);
|
||||||
|
const effectiveModel = effectiveModelEntry.model;
|
||||||
const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel);
|
const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setIsGeneratingPlan(false);
|
setIsGeneratingPlan(false);
|
||||||
@@ -381,8 +392,9 @@ export function BacklogPlanDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get effective model (override or global default) - extract model string from PhaseModelEntry
|
// Get effective model entry (override or global default)
|
||||||
const effectiveModel = modelOverride || extractModel(phaseModels.backlogPlanningModel);
|
const effectiveModelEntry = modelOverride || normalizeEntry(phaseModels.backlogPlanningModel);
|
||||||
|
const effectiveModel = effectiveModelEntry.model;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||||
@@ -407,7 +419,7 @@ export function BacklogPlanDialog({
|
|||||||
<div className="flex items-center gap-2 mr-auto">
|
<div className="flex items-center gap-2 mr-auto">
|
||||||
<span className="text-xs text-muted-foreground">Model:</span>
|
<span className="text-xs text-muted-foreground">Model:</span>
|
||||||
<ModelOverrideTrigger
|
<ModelOverrideTrigger
|
||||||
currentModel={effectiveModel}
|
currentModelEntry={effectiveModelEntry}
|
||||||
onModelChange={setModelOverride}
|
onModelChange={setModelOverride}
|
||||||
phase="backlogPlanningModel"
|
phase="backlogPlanningModel"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ export function EditFeatureDialog({
|
|||||||
const result = await api.enhancePrompt?.enhance(
|
const result = await api.enhancePrompt?.enhance(
|
||||||
editingFeature.description,
|
editingFeature.description,
|
||||||
enhancementMode,
|
enhancementMode,
|
||||||
enhancementOverride.effectiveModel
|
enhancementOverride.effectiveModel, // API accepts string, extract from PhaseModelEntry
|
||||||
|
enhancementOverride.effectiveModelEntry.thinkingLevel // Pass thinking level
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result?.success && result.enhancedText) {
|
if (result?.success && result.enhancedText) {
|
||||||
@@ -392,7 +393,7 @@ export function EditFeatureDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ModelOverrideTrigger
|
<ModelOverrideTrigger
|
||||||
currentModel={enhancementOverride.effectiveModel}
|
currentModelEntry={enhancementOverride.effectiveModelEntry}
|
||||||
onModelChange={enhancementOverride.setOverride}
|
onModelChange={enhancementOverride.setOverride}
|
||||||
phase="enhancementModel"
|
phase="enhancementModel"
|
||||||
isOverridden={enhancementOverride.isOverridden}
|
isOverridden={enhancementOverride.isOverridden}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export function GitHubIssuesView() {
|
|||||||
// Model override for validation
|
// Model override for validation
|
||||||
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
||||||
|
|
||||||
|
// Extract model string for API calls (backward compatibility)
|
||||||
|
const validationModelString = validationModelOverride.effectiveModel;
|
||||||
|
|
||||||
const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues();
|
const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues();
|
||||||
|
|
||||||
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
|
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function IssueDetailPanel({
|
|||||||
const getValidationOptions = (forceRevalidate = false) => {
|
const getValidationOptions = (forceRevalidate = false) => {
|
||||||
return {
|
return {
|
||||||
forceRevalidate,
|
forceRevalidate,
|
||||||
|
modelEntry: modelOverride.effectiveModelEntry, // Pass the full PhaseModelEntry to preserve thinking level
|
||||||
comments: includeCommentsInAnalysis && comments.length > 0 ? comments : undefined,
|
comments: includeCommentsInAnalysis && comments.length > 0 ? comments : undefined,
|
||||||
linkedPRs: issue.linkedPRs?.map((pr) => ({
|
linkedPRs: issue.linkedPRs?.map((pr) => ({
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
@@ -119,12 +120,13 @@ export function IssueDetailPanel({
|
|||||||
View (stale)
|
View (stale)
|
||||||
</Button>
|
</Button>
|
||||||
<ModelOverrideTrigger
|
<ModelOverrideTrigger
|
||||||
currentModel={modelOverride.effectiveModel}
|
currentModelEntry={modelOverride.effectiveModelEntry}
|
||||||
onModelChange={modelOverride.setOverride}
|
onModelChange={modelOverride.setOverride}
|
||||||
phase="validationModel"
|
phase="validationModel"
|
||||||
isOverridden={modelOverride.isOverridden}
|
isOverridden={modelOverride.isOverridden}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
className="mx-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -141,12 +143,13 @@ export function IssueDetailPanel({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModelOverrideTrigger
|
<ModelOverrideTrigger
|
||||||
currentModel={modelOverride.effectiveModel}
|
currentModelEntry={modelOverride.effectiveModelEntry}
|
||||||
onModelChange={modelOverride.setOverride}
|
onModelChange={modelOverride.setOverride}
|
||||||
phase="validationModel"
|
phase="validationModel"
|
||||||
isOverridden={modelOverride.isOverridden}
|
isOverridden={modelOverride.isOverridden}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
className="mr-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -227,12 +227,13 @@ export function useIssueValidation({
|
|||||||
issue: GitHubIssue,
|
issue: GitHubIssue,
|
||||||
options: {
|
options: {
|
||||||
forceRevalidate?: boolean;
|
forceRevalidate?: boolean;
|
||||||
model?: string;
|
model?: string | PhaseModelEntry; // Accept either string (backward compat) or PhaseModelEntry
|
||||||
|
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking level
|
||||||
comments?: GitHubComment[];
|
comments?: GitHubComment[];
|
||||||
linkedPRs?: LinkedPRInfo[];
|
linkedPRs?: LinkedPRInfo[];
|
||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const { forceRevalidate = false, model, comments, linkedPRs } = options;
|
const { forceRevalidate = false, model, modelEntry, comments, linkedPRs } = options;
|
||||||
|
|
||||||
if (!currentProject?.path) {
|
if (!currentProject?.path) {
|
||||||
toast.error('No project selected');
|
toast.error('No project selected');
|
||||||
@@ -260,8 +261,20 @@ export function useIssueValidation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use provided model override or fall back to phaseModels.validationModel
|
// Use provided model override or fall back to phaseModels.validationModel
|
||||||
// Extract model string from PhaseModelEntry (handles both old string format and new object format)
|
// Extract model string and thinking level from PhaseModelEntry (handles both old string format and new object format)
|
||||||
const modelToUse = model || extractModel(phaseModels.validationModel);
|
const effectiveModelEntry = modelEntry
|
||||||
|
? modelEntry
|
||||||
|
: model
|
||||||
|
? typeof model === 'string'
|
||||||
|
? { model: model as ModelAlias | CursorModelId }
|
||||||
|
: model
|
||||||
|
: phaseModels.validationModel;
|
||||||
|
const normalizedEntry =
|
||||||
|
typeof effectiveModelEntry === 'string'
|
||||||
|
? { model: effectiveModelEntry as ModelAlias | CursorModelId }
|
||||||
|
: effectiveModelEntry;
|
||||||
|
const modelToUse = normalizedEntry.model;
|
||||||
|
const thinkingLevelToUse = normalizedEntry.thinkingLevel;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -277,7 +290,8 @@ export function useIssueValidation({
|
|||||||
const result = await api.github.validateIssue(
|
const result = await api.github.validateIssue(
|
||||||
currentProject.path,
|
currentProject.path,
|
||||||
validationInput,
|
validationInput,
|
||||||
modelToUse
|
modelToUse,
|
||||||
|
thinkingLevelToUse
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -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 } from '@automaker/types';
|
import type { ModelAlias, CursorModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
|
||||||
|
|
||||||
export interface IssueRowProps {
|
export interface IssueRowProps {
|
||||||
issue: GitHubIssue;
|
issue: GitHubIssue;
|
||||||
@@ -36,8 +36,9 @@ export interface IssueDetailPanelProps {
|
|||||||
formatDate: (date: string) => string;
|
formatDate: (date: string) => string;
|
||||||
/** Model override state */
|
/** Model override state */
|
||||||
modelOverride: {
|
modelOverride: {
|
||||||
|
effectiveModelEntry: PhaseModelEntry;
|
||||||
effectiveModel: ModelAlias | CursorModelId;
|
effectiveModel: ModelAlias | CursorModelId;
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
setOverride: (model: ModelAlias | CursorModelId | null) => void;
|
setOverride: (entry: PhaseModelEntry | null) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface QueuedPrompt {
|
|||||||
message: string;
|
message: string;
|
||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
|
thinkingLevel?: string;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +521,13 @@ export function useElectronAgent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Adding to server queue');
|
logger.info('Adding to server queue');
|
||||||
const result = await api.agent.queueAdd(sessionId, messageContent, imagePaths, model);
|
const result = await api.agent.queueAdd(
|
||||||
|
sessionId,
|
||||||
|
messageContent,
|
||||||
|
imagePaths,
|
||||||
|
model,
|
||||||
|
thinkingLevel
|
||||||
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setError(result.error || 'Failed to add to queue');
|
setError(result.error || 'Failed to add to queue');
|
||||||
@@ -530,7 +537,7 @@ export function useElectronAgent({
|
|||||||
setError(err instanceof Error ? err.message : 'Failed to add to queue');
|
setError(err instanceof Error ? err.message : 'Failed to add to queue');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sessionId, workingDirectory, model]
|
[sessionId, workingDirectory, model, thinkingLevel]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove a prompt from the server queue
|
// Remove a prompt from the server queue
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ export interface GitHubAPI {
|
|||||||
validateIssue: (
|
validateIssue: (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
issue: IssueValidationInput,
|
issue: IssueValidationInput,
|
||||||
model?: ModelAlias
|
model?: ModelAlias,
|
||||||
|
thinkingLevel?: string
|
||||||
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
|
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
|
||||||
/** Check validation status for an issue or all issues */
|
/** Check validation status for an issue or all issues */
|
||||||
getValidationStatus: (
|
getValidationStatus: (
|
||||||
|
|||||||
@@ -1229,12 +1229,14 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
enhance: (
|
enhance: (
|
||||||
originalText: string,
|
originalText: string,
|
||||||
enhancementMode: string,
|
enhancementMode: string,
|
||||||
model?: string
|
model?: string,
|
||||||
|
thinkingLevel?: string
|
||||||
): Promise<EnhancePromptResult> =>
|
): Promise<EnhancePromptResult> =>
|
||||||
this.post('/api/enhance-prompt', {
|
this.post('/api/enhance-prompt', {
|
||||||
originalText,
|
originalText,
|
||||||
enhancementMode,
|
enhancementMode,
|
||||||
model,
|
model,
|
||||||
|
thinkingLevel,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1374,8 +1376,12 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }),
|
checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }),
|
||||||
listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }),
|
listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }),
|
||||||
listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }),
|
listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }),
|
||||||
validateIssue: (projectPath: string, issue: IssueValidationInput, model?: string) =>
|
validateIssue: (
|
||||||
this.post('/api/github/validate-issue', { projectPath, ...issue, model }),
|
projectPath: string,
|
||||||
|
issue: IssueValidationInput,
|
||||||
|
model?: string,
|
||||||
|
thinkingLevel?: string
|
||||||
|
) => this.post('/api/github/validate-issue', { projectPath, ...issue, model, thinkingLevel }),
|
||||||
getValidationStatus: (projectPath: string, issueNumber?: number) =>
|
getValidationStatus: (projectPath: string, issueNumber?: number) =>
|
||||||
this.post('/api/github/validation-status', { projectPath, issueNumber }),
|
this.post('/api/github/validation-status', { projectPath, issueNumber }),
|
||||||
stopValidation: (projectPath: string, issueNumber: number) =>
|
stopValidation: (projectPath: string, issueNumber: number) =>
|
||||||
@@ -1459,7 +1465,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
message: string,
|
message: string,
|
||||||
imagePaths?: string[],
|
imagePaths?: string[],
|
||||||
model?: string
|
model?: string,
|
||||||
|
thinkingLevel?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
queuedPrompt?: {
|
queuedPrompt?: {
|
||||||
@@ -1467,10 +1474,12 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
message: string;
|
message: string;
|
||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
|
thinkingLevel?: string;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
}> => this.post('/api/agent/queue/add', { sessionId, message, imagePaths, model }),
|
}> =>
|
||||||
|
this.post('/api/agent/queue/add', { sessionId, message, imagePaths, model, thinkingLevel }),
|
||||||
|
|
||||||
queueList: (
|
queueList: (
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -1481,6 +1490,7 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
message: string;
|
message: string;
|
||||||
imagePaths?: string[];
|
imagePaths?: string[];
|
||||||
model?: string;
|
model?: string;
|
||||||
|
thinkingLevel?: string;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
}>;
|
}>;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
import { spawn, type ChildProcess } from 'child_process';
|
import { spawn, type ChildProcess } from 'child_process';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
|
||||||
|
const logger = createLogger('SubprocessManager');
|
||||||
|
|
||||||
export interface SubprocessOptions {
|
export interface SubprocessOptions {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -38,10 +41,10 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Log command without stdin data (which may be large/sensitive)
|
// Log command without stdin data (which may be large/sensitive)
|
||||||
console.log(`[SubprocessManager] Spawning: ${command} ${args.join(' ')}`);
|
logger.info(`Spawning: ${command} ${args.join(' ')}`);
|
||||||
console.log(`[SubprocessManager] Working directory: ${cwd}`);
|
logger.info(`Working directory: ${cwd}`);
|
||||||
if (stdinData) {
|
if (stdinData) {
|
||||||
console.log(`[SubprocessManager] Passing ${stdinData.length} bytes via stdin`);
|
logger.info(`Passing ${stdinData.length} bytes via stdin`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const childProcess: ChildProcess = spawn(command, args, {
|
const childProcess: ChildProcess = spawn(command, args, {
|
||||||
@@ -66,7 +69,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
childProcess.stderr.on('data', (data: Buffer) => {
|
childProcess.stderr.on('data', (data: Buffer) => {
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
stderrOutput += text;
|
stderrOutput += text;
|
||||||
console.error(`[SubprocessManager] stderr: ${text}`);
|
logger.warn(`stderr: ${text}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
timeoutHandle = setTimeout(() => {
|
timeoutHandle = setTimeout(() => {
|
||||||
const elapsed = Date.now() - lastOutputTime;
|
const elapsed = Date.now() - lastOutputTime;
|
||||||
if (elapsed >= timeout) {
|
if (elapsed >= timeout) {
|
||||||
console.error(`[SubprocessManager] Process timeout: no output for ${timeout}ms`);
|
logger.error(`Process timeout: no output for ${timeout}ms`);
|
||||||
childProcess.kill('SIGTERM');
|
childProcess.kill('SIGTERM');
|
||||||
}
|
}
|
||||||
}, timeout);
|
}, timeout);
|
||||||
@@ -90,7 +93,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
// Setup abort handling
|
// Setup abort handling
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
abortController.signal.addEventListener('abort', () => {
|
abortController.signal.addEventListener('abort', () => {
|
||||||
console.log('[SubprocessManager] Abort signal received, killing process');
|
logger.info('Abort signal received, killing process');
|
||||||
if (timeoutHandle) {
|
if (timeoutHandle) {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
}
|
}
|
||||||
@@ -115,7 +118,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
const parsed = JSON.parse(line);
|
const parsed = JSON.parse(line);
|
||||||
yield parsed;
|
yield parsed;
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error(`[SubprocessManager] Failed to parse JSONL line: ${line}`, parseError);
|
logger.error(`Failed to parse JSONL line: ${line}`, parseError);
|
||||||
// Yield error but continue processing
|
// Yield error but continue processing
|
||||||
yield {
|
yield {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -124,7 +127,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SubprocessManager] Error reading stdout:', error);
|
logger.error('Error reading stdout:', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutHandle) {
|
if (timeoutHandle) {
|
||||||
@@ -136,12 +139,12 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
// Wait for process to exit
|
// Wait for process to exit
|
||||||
const exitCode = await new Promise<number | null>((resolve) => {
|
const exitCode = await new Promise<number | null>((resolve) => {
|
||||||
childProcess.on('exit', (code) => {
|
childProcess.on('exit', (code) => {
|
||||||
console.log(`[SubprocessManager] Process exited with code: ${code}`);
|
logger.info(`Process exited with code: ${code}`);
|
||||||
resolve(code);
|
resolve(code);
|
||||||
});
|
});
|
||||||
|
|
||||||
childProcess.on('error', (error) => {
|
childProcess.on('error', (error) => {
|
||||||
console.error('[SubprocessManager] Process error:', error);
|
logger.error('Process error:', error);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -149,7 +152,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
// Handle non-zero exit codes
|
// Handle non-zero exit codes
|
||||||
if (exitCode !== 0 && exitCode !== null) {
|
if (exitCode !== 0 && exitCode !== null) {
|
||||||
const errorMessage = stderrOutput || `Process exited with code ${exitCode}`;
|
const errorMessage = stderrOutput || `Process exited with code ${exitCode}`;
|
||||||
console.error(`[SubprocessManager] Process failed: ${errorMessage}`);
|
logger.error(`Process failed: ${errorMessage}`);
|
||||||
yield {
|
yield {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -158,7 +161,7 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
|
|||||||
|
|
||||||
// Process completed successfully
|
// Process completed successfully
|
||||||
if (exitCode === 0 && !stderrOutput) {
|
if (exitCode === 0 && !stderrOutput) {
|
||||||
console.log('[SubprocessManager] Process completed successfully');
|
logger.info('Process completed successfully');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user