feat(insights): add per-session model and thinking level selection
- Add model selector dropdown in Insights chat header with agent profiles: - Complex (Opus + ultrathink) - Balanced (Sonnet + medium) - new default - Quick (Haiku + low) - Custom option for direct model + thinking level selection - Change default from slow Opus to Sonnet for faster responses - Persist model configuration per-session - Fix missing event forwarding from insightsService to renderer (was causing responses to not appear without hard refresh) - Fix insights_runner.py path (was looking in auto-claude/ instead of auto-claude/runners/) - Add --model and --thinking-level CLI args to insights_runner.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@ import { EventEmitter } from 'events';
|
||||
import type {
|
||||
InsightsSession,
|
||||
InsightsSessionSummary,
|
||||
InsightsChatMessage
|
||||
InsightsChatMessage,
|
||||
InsightsModelConfig
|
||||
} from '../shared/types';
|
||||
import { InsightsConfig } from './insights/config';
|
||||
import { InsightsPaths } from './insights/paths';
|
||||
@@ -111,7 +112,12 @@ export class InsightsService extends EventEmitter {
|
||||
/**
|
||||
* Send a message and get AI response
|
||||
*/
|
||||
async sendMessage(projectId: string, projectPath: string, message: string): Promise<void> {
|
||||
async sendMessage(
|
||||
projectId: string,
|
||||
projectPath: string,
|
||||
message: string,
|
||||
modelConfig?: InsightsModelConfig
|
||||
): Promise<void> {
|
||||
// Cancel any existing session
|
||||
this.executor.cancelSession(projectId);
|
||||
|
||||
@@ -150,13 +156,17 @@ export class InsightsService extends EventEmitter {
|
||||
content: m.content
|
||||
}));
|
||||
|
||||
// Use provided modelConfig or fall back to session's config
|
||||
const configToUse = modelConfig || session.modelConfig;
|
||||
|
||||
try {
|
||||
// Execute insights query
|
||||
const result = await this.executor.execute(
|
||||
projectId,
|
||||
projectPath,
|
||||
message,
|
||||
conversationHistory
|
||||
conversationHistory,
|
||||
configToUse
|
||||
);
|
||||
|
||||
// Add assistant message to session
|
||||
@@ -177,6 +187,13 @@ export class InsightsService extends EventEmitter {
|
||||
console.error('[InsightsService] Error executing insights:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model configuration for a session
|
||||
*/
|
||||
updateSessionModelConfig(projectPath: string, sessionId: string, modelConfig: InsightsModelConfig): boolean {
|
||||
return this.sessionManager.updateSessionModelConfig(projectPath, sessionId, modelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
@@ -6,8 +6,10 @@ import type {
|
||||
InsightsChatMessage,
|
||||
InsightsChatStatus,
|
||||
InsightsStreamChunk,
|
||||
InsightsToolUsage
|
||||
InsightsToolUsage,
|
||||
InsightsModelConfig
|
||||
} from '../../shared/types';
|
||||
import { MODEL_ID_MAP } from '../../shared/constants';
|
||||
import { InsightsConfig } from './config';
|
||||
import { detectRateLimit, createSDKRateLimitInfo } from '../rate-limit-detector';
|
||||
|
||||
@@ -59,7 +61,8 @@ export class InsightsExecutor extends EventEmitter {
|
||||
projectId: string,
|
||||
projectPath: string,
|
||||
message: string,
|
||||
conversationHistory: Array<{ role: string; content: string }>
|
||||
conversationHistory: Array<{ role: string; content: string }>,
|
||||
modelConfig?: InsightsModelConfig
|
||||
): Promise<ProcessorResult> {
|
||||
// Cancel any existing session
|
||||
this.cancelSession(projectId);
|
||||
@@ -69,7 +72,7 @@ export class InsightsExecutor extends EventEmitter {
|
||||
throw new Error('Auto Claude source not found');
|
||||
}
|
||||
|
||||
const runnerPath = path.join(autoBuildSource, 'insights_runner.py');
|
||||
const runnerPath = path.join(autoBuildSource, 'runners', 'insights_runner.py');
|
||||
if (!existsSync(runnerPath)) {
|
||||
throw new Error('insights_runner.py not found in auto-claude directory');
|
||||
}
|
||||
@@ -83,13 +86,23 @@ export class InsightsExecutor extends EventEmitter {
|
||||
// Get process environment
|
||||
const processEnv = this.config.getProcessEnv();
|
||||
|
||||
// Spawn Python process
|
||||
const proc = spawn(this.config.getPythonPath(), [
|
||||
// Build command arguments
|
||||
const args = [
|
||||
runnerPath,
|
||||
'--project-dir', projectPath,
|
||||
'--message', message,
|
||||
'--history', JSON.stringify(conversationHistory)
|
||||
], {
|
||||
];
|
||||
|
||||
// Add model config if provided
|
||||
if (modelConfig) {
|
||||
const modelId = MODEL_ID_MAP[modelConfig.model] || MODEL_ID_MAP['sonnet'];
|
||||
args.push('--model', modelId);
|
||||
args.push('--thinking-level', modelConfig.thinkingLevel);
|
||||
}
|
||||
|
||||
// Spawn Python process
|
||||
const proc = spawn(this.config.getPythonPath(), args, {
|
||||
cwd: autoBuildSource,
|
||||
env: processEnv
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InsightsSession, InsightsSessionSummary } from '../../shared/types';
|
||||
import type { InsightsSession, InsightsSessionSummary, InsightsModelConfig } from '../../shared/types';
|
||||
import { SessionStorage } from './session-storage';
|
||||
import { InsightsPaths } from './paths';
|
||||
|
||||
@@ -119,6 +119,30 @@ export class SessionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model configuration for a session
|
||||
*/
|
||||
updateSessionModelConfig(projectPath: string, sessionId: string, modelConfig: InsightsModelConfig): boolean {
|
||||
const session = this.storage.loadSessionById(projectPath, sessionId);
|
||||
if (!session) return false;
|
||||
|
||||
session.modelConfig = modelConfig;
|
||||
session.updatedAt = new Date();
|
||||
this.storage.saveSession(projectPath, session);
|
||||
|
||||
// Update cache if this session is cached
|
||||
for (const [projectId, cachedSession] of this.sessions) {
|
||||
if (cachedSession.id === sessionId) {
|
||||
cachedSession.modelConfig = modelConfig;
|
||||
cachedSession.updatedAt = new Date();
|
||||
this.sessions.set(projectId, cachedSession);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save session to disk and update cache
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BrowserWindow } from 'electron';
|
||||
import path from 'path';
|
||||
import { existsSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { IPC_CHANNELS, getSpecsDir, AUTO_BUILD_PATHS } from '../../shared/constants';
|
||||
import type { IPCResult, InsightsSession, InsightsSessionSummary, Task, TaskMetadata } from '../../shared/types';
|
||||
import type { IPCResult, InsightsSession, InsightsSessionSummary, InsightsModelConfig, Task, TaskMetadata } from '../../shared/types';
|
||||
import { projectStore } from '../project-store';
|
||||
import { insightsService } from '../insights-service';
|
||||
|
||||
@@ -32,7 +32,7 @@ export function registerInsightsHandlers(
|
||||
|
||||
ipcMain.on(
|
||||
IPC_CHANNELS.INSIGHTS_SEND_MESSAGE,
|
||||
async (_, projectId: string, message: string) => {
|
||||
async (_, projectId: string, message: string, modelConfig?: InsightsModelConfig) => {
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
const mainWindow = getMainWindow();
|
||||
@@ -44,7 +44,7 @@ export function registerInsightsHandlers(
|
||||
|
||||
// Note: Python environment initialization should be handled by insightsService
|
||||
// or added here with proper dependency injection if needed
|
||||
insightsService.sendMessage(projectId, project.path, message);
|
||||
insightsService.sendMessage(projectId, project.path, message, modelConfig);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -241,4 +241,57 @@ export function registerInsightsHandlers(
|
||||
}
|
||||
);
|
||||
|
||||
// Update model configuration for a session
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INSIGHTS_UPDATE_MODEL_CONFIG,
|
||||
async (_, projectId: string, sessionId: string, modelConfig: InsightsModelConfig): Promise<IPCResult> => {
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
return { success: false, error: 'Project not found' };
|
||||
}
|
||||
|
||||
const success = insightsService.updateSessionModelConfig(project.path, sessionId, modelConfig);
|
||||
if (success) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: 'Failed to update model configuration' };
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Insights Event Forwarding (Service -> Renderer)
|
||||
// ============================================
|
||||
|
||||
// Forward streaming chunks to renderer
|
||||
insightsService.on('stream-chunk', (projectId: string, chunk: unknown) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_STREAM_CHUNK, projectId, chunk);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward status updates to renderer
|
||||
insightsService.on('status', (projectId: string, status: unknown) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_STATUS, projectId, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward errors to renderer
|
||||
insightsService.on('error', (projectId: string, error: string) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.INSIGHTS_ERROR, projectId, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward SDK rate limit events to renderer
|
||||
insightsService.on('sdk-rate-limit', (rateLimitInfo: unknown) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.CLAUDE_SDK_RATE_LIMIT, rateLimitInfo);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
InsightsSessionSummary,
|
||||
InsightsChatStatus,
|
||||
InsightsStreamChunk,
|
||||
InsightsModelConfig,
|
||||
Task,
|
||||
TaskMetadata,
|
||||
IPCResult
|
||||
@@ -16,7 +17,7 @@ import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc
|
||||
export interface InsightsAPI {
|
||||
// Operations
|
||||
getInsightsSession: (projectId: string) => Promise<IPCResult<InsightsSession | null>>;
|
||||
sendInsightsMessage: (projectId: string, message: string) => void;
|
||||
sendInsightsMessage: (projectId: string, message: string, modelConfig?: InsightsModelConfig) => void;
|
||||
clearInsightsSession: (projectId: string) => Promise<IPCResult>;
|
||||
createTaskFromInsights: (
|
||||
projectId: string,
|
||||
@@ -29,6 +30,7 @@ export interface InsightsAPI {
|
||||
switchInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult<InsightsSession | null>>;
|
||||
deleteInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult>;
|
||||
renameInsightsSession: (projectId: string, sessionId: string, newTitle: string) => Promise<IPCResult>;
|
||||
updateInsightsModelConfig: (projectId: string, sessionId: string, modelConfig: InsightsModelConfig) => Promise<IPCResult>;
|
||||
|
||||
// Event Listeners
|
||||
onInsightsStreamChunk: (
|
||||
@@ -50,8 +52,8 @@ export const createInsightsAPI = (): InsightsAPI => ({
|
||||
getInsightsSession: (projectId: string): Promise<IPCResult<InsightsSession | null>> =>
|
||||
invokeIpc(IPC_CHANNELS.INSIGHTS_GET_SESSION, projectId),
|
||||
|
||||
sendInsightsMessage: (projectId: string, message: string): void =>
|
||||
sendIpc(IPC_CHANNELS.INSIGHTS_SEND_MESSAGE, projectId, message),
|
||||
sendInsightsMessage: (projectId: string, message: string, modelConfig?: InsightsModelConfig): void =>
|
||||
sendIpc(IPC_CHANNELS.INSIGHTS_SEND_MESSAGE, projectId, message, modelConfig),
|
||||
|
||||
clearInsightsSession: (projectId: string): Promise<IPCResult> =>
|
||||
invokeIpc(IPC_CHANNELS.INSIGHTS_CLEAR_SESSION, projectId),
|
||||
@@ -79,6 +81,9 @@ export const createInsightsAPI = (): InsightsAPI => ({
|
||||
renameInsightsSession: (projectId: string, sessionId: string, newTitle: string): Promise<IPCResult> =>
|
||||
invokeIpc(IPC_CHANNELS.INSIGHTS_RENAME_SESSION, projectId, sessionId, newTitle),
|
||||
|
||||
updateInsightsModelConfig: (projectId: string, sessionId: string, modelConfig: InsightsModelConfig): Promise<IPCResult> =>
|
||||
invokeIpc(IPC_CHANNELS.INSIGHTS_UPDATE_MODEL_CONFIG, projectId, sessionId, modelConfig),
|
||||
|
||||
// Event Listeners
|
||||
onInsightsStreamChunk: (
|
||||
callback: (projectId: string, chunk: InsightsStreamChunk) => void
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogDescription
|
||||
} from './ui/dialog';
|
||||
import { Button } from './ui/button';
|
||||
import { Label } from './ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from './ui/select';
|
||||
import { AVAILABLE_MODELS, THINKING_LEVELS } from '../../shared/constants';
|
||||
import type { InsightsModelConfig } from '../../shared/types';
|
||||
import type { ModelType, ThinkingLevel } from '../../shared/types';
|
||||
|
||||
interface CustomModelModalProps {
|
||||
currentConfig?: InsightsModelConfig;
|
||||
onSave: (config: InsightsModelConfig) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CustomModelModal({ currentConfig, onSave, onClose }: CustomModelModalProps) {
|
||||
const [model, setModel] = useState<ModelType>(
|
||||
currentConfig?.model || 'sonnet'
|
||||
);
|
||||
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel>(
|
||||
currentConfig?.thinkingLevel || 'medium'
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
profileId: 'custom',
|
||||
model,
|
||||
thinkingLevel
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Custom Model Configuration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure the model and thinking level for this chat session.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="model-select">Model</Label>
|
||||
<Select value={model} onValueChange={(v) => setModel(v as ModelType)}>
|
||||
<SelectTrigger id="model-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_MODELS.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thinking-select">Thinking Level</Label>
|
||||
<Select value={thinkingLevel} onValueChange={(v) => setThinkingLevel(v as ThinkingLevel)}>
|
||||
<SelectTrigger id="thinking-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{level.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{level.description}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Apply
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -29,12 +29,14 @@ import {
|
||||
switchSession,
|
||||
deleteSession,
|
||||
renameSession,
|
||||
updateModelConfig,
|
||||
createTaskFromSuggestion,
|
||||
setupInsightsListeners
|
||||
} from '../stores/insights-store';
|
||||
import { loadTasks } from '../stores/task-store';
|
||||
import { ChatHistorySidebar } from './ChatHistorySidebar';
|
||||
import type { InsightsChatMessage } from '../../shared/types';
|
||||
import { InsightsModelSelector } from './InsightsModelSelector';
|
||||
import type { InsightsChatMessage, InsightsModelConfig } from '../../shared/types';
|
||||
import {
|
||||
TASK_CATEGORY_LABELS,
|
||||
TASK_CATEGORY_COLORS,
|
||||
@@ -141,6 +143,13 @@ export function Insights({ projectId }: InsightsProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelConfigChange = async (config: InsightsModelConfig) => {
|
||||
// If we have a session, persist the config
|
||||
if (session?.id) {
|
||||
await updateModelConfig(projectId, session.id, config);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = status.phase === 'thinking' || status.phase === 'streaming';
|
||||
const messages = session?.messages || [];
|
||||
|
||||
@@ -187,14 +196,21 @@ export function Insights({ projectId }: InsightsProps) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNewSession}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Chat
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<InsightsModelSelector
|
||||
currentConfig={session?.modelConfig}
|
||||
onConfigChange={handleModelConfigChange}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNewSession}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useState } from 'react';
|
||||
import { Brain, Scale, Zap, Sliders, Check } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuLabel
|
||||
} from './ui/dropdown-menu';
|
||||
import { DEFAULT_AGENT_PROFILES, AVAILABLE_MODELS } from '../../shared/constants';
|
||||
import type { InsightsModelConfig } from '../../shared/types';
|
||||
import { CustomModelModal } from './CustomModelModal';
|
||||
|
||||
interface InsightsModelSelectorProps {
|
||||
currentConfig?: InsightsModelConfig;
|
||||
onConfigChange: (config: InsightsModelConfig) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, React.ElementType> = {
|
||||
Brain,
|
||||
Scale,
|
||||
Zap
|
||||
};
|
||||
|
||||
export function InsightsModelSelector({
|
||||
currentConfig,
|
||||
onConfigChange,
|
||||
disabled
|
||||
}: InsightsModelSelectorProps) {
|
||||
const [showCustomModal, setShowCustomModal] = useState(false);
|
||||
|
||||
// Default to 'balanced' if no config
|
||||
const selectedProfileId = currentConfig?.profileId || 'balanced';
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedProfileId);
|
||||
|
||||
// Get the appropriate icon
|
||||
const Icon = selectedProfileId === 'custom'
|
||||
? Sliders
|
||||
: (profile?.icon ? iconMap[profile.icon] : Scale);
|
||||
|
||||
const handleSelectProfile = (profileId: string) => {
|
||||
if (profileId === 'custom') {
|
||||
setShowCustomModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = DEFAULT_AGENT_PROFILES.find(p => p.id === profileId);
|
||||
if (selected) {
|
||||
onConfigChange({
|
||||
profileId: selected.id,
|
||||
model: selected.model,
|
||||
thinkingLevel: selected.thinkingLevel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomSave = (config: InsightsModelConfig) => {
|
||||
onConfigChange(config);
|
||||
setShowCustomModal(false);
|
||||
};
|
||||
|
||||
// Build display text for current selection
|
||||
const getDisplayText = () => {
|
||||
if (selectedProfileId === 'custom' && currentConfig) {
|
||||
const modelLabel = AVAILABLE_MODELS.find(m => m.value === currentConfig.model)?.label || currentConfig.model;
|
||||
return `${modelLabel} + ${currentConfig.thinkingLevel}`;
|
||||
}
|
||||
return profile?.name || 'Balanced';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-2 px-2"
|
||||
disabled={disabled}
|
||||
title={`Model: ${getDisplayText()}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="hidden text-xs text-muted-foreground sm:inline">
|
||||
{getDisplayText()}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel>Agent Profile</DropdownMenuLabel>
|
||||
{DEFAULT_AGENT_PROFILES.map((p) => {
|
||||
const ProfileIcon = iconMap[p.icon || 'Brain'];
|
||||
const isSelected = selectedProfileId === p.id;
|
||||
const modelLabel = AVAILABLE_MODELS.find(m => m.value === p.model)?.label;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={p.id}
|
||||
onClick={() => handleSelectProfile(p.id)}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<ProfileIcon className="h-4 w-4 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{modelLabel} + {p.thinkingLevel}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectProfile('custom')}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<Sliders className="h-4 w-4 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Custom...</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Choose model & thinking level
|
||||
</div>
|
||||
</div>
|
||||
{selectedProfileId === 'custom' && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{showCustomModal && (
|
||||
<CustomModelModal
|
||||
currentConfig={currentConfig}
|
||||
onSave={handleCustomSave}
|
||||
onClose={() => setShowCustomModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
InsightsChatStatus,
|
||||
InsightsStreamChunk,
|
||||
InsightsToolUsage,
|
||||
InsightsModelConfig,
|
||||
TaskMetadata,
|
||||
Task
|
||||
} from '../../shared/types';
|
||||
@@ -221,8 +222,9 @@ export async function loadInsightsSession(projectId: string): Promise<void> {
|
||||
await loadInsightsSessions(projectId);
|
||||
}
|
||||
|
||||
export function sendMessage(projectId: string, message: string): void {
|
||||
export function sendMessage(projectId: string, message: string, modelConfig?: InsightsModelConfig): void {
|
||||
const store = useInsightsStore.getState();
|
||||
const session = store.session;
|
||||
|
||||
// Add user message to session
|
||||
const userMessage: InsightsChatMessage = {
|
||||
@@ -242,8 +244,11 @@ export function sendMessage(projectId: string, message: string): void {
|
||||
message: 'Processing your message...'
|
||||
});
|
||||
|
||||
// Use provided modelConfig, or fall back to session's config
|
||||
const configToUse = modelConfig || session?.modelConfig;
|
||||
|
||||
// Send to main process
|
||||
window.electronAPI.sendInsightsMessage(projectId, message);
|
||||
window.electronAPI.sendInsightsMessage(projectId, message, configToUse);
|
||||
}
|
||||
|
||||
export async function clearSession(projectId: string): Promise<void> {
|
||||
@@ -296,6 +301,25 @@ export async function renameSession(projectId: string, sessionId: string, newTit
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function updateModelConfig(projectId: string, sessionId: string, modelConfig: InsightsModelConfig): Promise<boolean> {
|
||||
const result = await window.electronAPI.updateInsightsModelConfig(projectId, sessionId, modelConfig);
|
||||
if (result.success) {
|
||||
// Update local session state
|
||||
const store = useInsightsStore.getState();
|
||||
if (store.session?.id === sessionId) {
|
||||
store.setSession({
|
||||
...store.session,
|
||||
modelConfig,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
// Reload sessions list to reflect the change
|
||||
await loadInsightsSessions(projectId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function createTaskFromSuggestion(
|
||||
projectId: string,
|
||||
title: string,
|
||||
|
||||
@@ -248,6 +248,7 @@ export const IPC_CHANNELS = {
|
||||
INSIGHTS_SWITCH_SESSION: 'insights:switchSession',
|
||||
INSIGHTS_DELETE_SESSION: 'insights:deleteSession',
|
||||
INSIGHTS_RENAME_SESSION: 'insights:renameSession',
|
||||
INSIGHTS_UPDATE_MODEL_CONFIG: 'insights:updateModelConfig',
|
||||
|
||||
// Insights events (main -> renderer)
|
||||
INSIGHTS_STREAM_CHUNK: 'insights:streamChunk',
|
||||
|
||||
@@ -15,6 +15,22 @@ export const AVAILABLE_MODELS = [
|
||||
{ value: 'haiku', label: 'Claude Haiku 4.5' }
|
||||
] as const;
|
||||
|
||||
// Maps model shorthand to actual Claude model IDs
|
||||
export const MODEL_ID_MAP: Record<string, string> = {
|
||||
opus: 'claude-opus-4-5-20251101',
|
||||
sonnet: 'claude-sonnet-4-5-20250929',
|
||||
haiku: 'claude-haiku-4-5-20250929'
|
||||
} as const;
|
||||
|
||||
// Maps thinking levels to budget tokens (null = no extended thinking)
|
||||
export const THINKING_BUDGET_MAP: Record<string, number | null> = {
|
||||
none: null,
|
||||
low: 1024,
|
||||
medium: 4096,
|
||||
high: 16384,
|
||||
ultrathink: 65536
|
||||
} as const;
|
||||
|
||||
// ============================================
|
||||
// Thinking Levels
|
||||
// ============================================
|
||||
|
||||
@@ -154,6 +154,16 @@ export interface IdeationSummary {
|
||||
// Insights Chat Types
|
||||
// ============================================
|
||||
|
||||
import type { ThinkingLevel } from './settings';
|
||||
import type { ModelType } from './task';
|
||||
|
||||
// Model configuration for insights sessions
|
||||
export interface InsightsModelConfig {
|
||||
profileId: string; // 'complex' | 'balanced' | 'quick' | 'custom'
|
||||
model: ModelType; // 'haiku' | 'sonnet' | 'opus'
|
||||
thinkingLevel: ThinkingLevel;
|
||||
}
|
||||
|
||||
export type InsightsChatRole = 'user' | 'assistant';
|
||||
|
||||
// Tool usage record for showing what tools the AI used
|
||||
@@ -183,6 +193,7 @@ export interface InsightsSession {
|
||||
projectId: string;
|
||||
title?: string; // Auto-generated from first message or user-set
|
||||
messages: InsightsChatMessage[];
|
||||
modelConfig?: InsightsModelConfig; // Per-session model configuration
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -193,6 +204,7 @@ export interface InsightsSessionSummary {
|
||||
projectId: string;
|
||||
title: string;
|
||||
messageCount: number;
|
||||
modelConfig?: InsightsModelConfig; // For displaying model indicator in sidebar
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,13 @@ Be conversational and helpful. Focus on providing actionable insights and clear
|
||||
Keep responses concise but informative."""
|
||||
|
||||
|
||||
async def run_with_sdk(project_dir: str, message: str, history: list) -> None:
|
||||
async def run_with_sdk(
|
||||
project_dir: str,
|
||||
message: str,
|
||||
history: list,
|
||||
model: str = "claude-sonnet-4-5-20250929",
|
||||
thinking_level: str = "medium"
|
||||
) -> None:
|
||||
"""Run the chat using Claude SDK with streaming."""
|
||||
if not SDK_AVAILABLE:
|
||||
print("Claude SDK not available, falling back to simple mode", file=sys.stderr)
|
||||
@@ -163,11 +169,18 @@ async def run_with_sdk(project_dir: str, message: str, history: list) -> None:
|
||||
|
||||
Current question: {message}"""
|
||||
|
||||
debug(
|
||||
"insights_runner",
|
||||
"Using model configuration",
|
||||
model=model,
|
||||
thinking_level=thinking_level,
|
||||
)
|
||||
|
||||
try:
|
||||
# Create Claude SDK client with appropriate settings for insights
|
||||
client = ClaudeSDKClient(
|
||||
options=ClaudeAgentOptions(
|
||||
model="claude-opus-4-5-20251101", # Opus 4.5 for quality analysis
|
||||
model=model, # Use configured model
|
||||
system_prompt=system_prompt,
|
||||
allowed_tools=[
|
||||
"Read",
|
||||
@@ -318,18 +331,33 @@ def main():
|
||||
parser.add_argument("--project-dir", required=True, help="Project directory path")
|
||||
parser.add_argument("--message", required=True, help="User message")
|
||||
parser.add_argument("--history", default="[]", help="JSON conversation history")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="claude-sonnet-4-5-20250929",
|
||||
help="Claude model ID (default: claude-sonnet-4-5-20250929)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--thinking-level",
|
||||
default="medium",
|
||||
choices=["none", "low", "medium", "high", "ultrathink"],
|
||||
help="Thinking level for extended reasoning (default: medium)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
debug_section("insights_runner", "Starting Insights Chat")
|
||||
|
||||
project_dir = args.project_dir
|
||||
user_message = args.message
|
||||
model = args.model
|
||||
thinking_level = args.thinking_level
|
||||
|
||||
debug(
|
||||
"insights_runner",
|
||||
"Arguments",
|
||||
project_dir=project_dir,
|
||||
message_length=len(user_message),
|
||||
model=model,
|
||||
thinking_level=thinking_level,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -341,7 +369,7 @@ def main():
|
||||
|
||||
# Run the async SDK function
|
||||
debug("insights_runner", "Running SDK query")
|
||||
asyncio.run(run_with_sdk(project_dir, user_message, history))
|
||||
asyncio.run(run_with_sdk(project_dir, user_message, history, model, thinking_level))
|
||||
debug_success("insights_runner", "Query completed")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user