Merge v0.8.0rc into feat/cursor-cli

Resolved conflicts:
- sdk-options.ts: kept HEAD (MCP & thinking level features)
- auto-mode-service.ts: kept HEAD (MCP features + fallback code)
- agent-output-modal.tsx: used v0.8.0rc (effectiveViewMode + pr-8 spacing)
- feature-suggestions-dialog.tsx: accepted deletion
- electron.ts: used v0.8.0rc (Ideation types)
- package-lock.json: regenerated

Fixed sdk-options.test.ts to expect 'default' permissionMode for read-only operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2026-01-04 13:12:45 +01:00
81 changed files with 6933 additions and 1958 deletions

View File

@@ -39,55 +39,118 @@ export function formatModelName(model: string): string {
return model.split('-').slice(1, 3).join(' ');
}
/**
* Helper to extract a balanced JSON object from a string starting at a given position
*/
function extractJsonObject(str: string, startIdx: number): string | null {
if (str[startIdx] !== '{') return null;
let depth = 0;
let inString = false;
let escapeNext = false;
for (let i = startIdx; i < str.length; i++) {
const char = str[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\' && inString) {
escapeNext = true;
continue;
}
if (char === '"' && !escapeNext) {
inString = !inString;
continue;
}
if (inString) continue;
if (char === '{') depth++;
else if (char === '}') {
depth--;
if (depth === 0) {
return str.slice(startIdx, i + 1);
}
}
}
return null;
}
/**
* Extracts todos from the context content
* Looks for TodoWrite tool calls in the format:
* TodoWrite: [{"content": "...", "status": "..."}]
* 🔧 Tool: TodoWrite
* Input: {"todos": [{"content": "...", "status": "..."}]}
*/
function extractTodos(content: string): AgentTaskInfo['todos'] {
const todos: AgentTaskInfo['todos'] = [];
// Look for TodoWrite tool inputs
const todoMatches = content.matchAll(
/TodoWrite.*?(?:"todos"\s*:\s*)?(\[[\s\S]*?\](?=\s*(?:\}|$|🔧|📋|⚡|✅|❌)))/g
);
// Find all occurrences of TodoWrite tool calls
const todoWriteMarker = '🔧 Tool: TodoWrite';
let searchStart = 0;
for (const match of todoMatches) {
try {
// Try to find JSON array in the match
const jsonStr = match[1] || match[0];
const arrayMatch = jsonStr.match(/\[[\s\S]*?\]/);
if (arrayMatch) {
const parsed = JSON.parse(arrayMatch[0]);
if (Array.isArray(parsed)) {
for (const item of parsed) {
while (true) {
const markerIdx = content.indexOf(todoWriteMarker, searchStart);
if (markerIdx === -1) break;
// Look for "Input:" after the marker
const inputIdx = content.indexOf('Input:', markerIdx);
if (inputIdx === -1 || inputIdx > markerIdx + 100) {
searchStart = markerIdx + 1;
continue;
}
// Find the start of the JSON object
const jsonStart = content.indexOf('{', inputIdx);
if (jsonStart === -1) {
searchStart = markerIdx + 1;
continue;
}
// Extract the complete JSON object
const jsonStr = extractJsonObject(content, jsonStart);
if (jsonStr) {
try {
const parsed = JSON.parse(jsonStr) as {
todos?: Array<{ content: string; status: string }>;
};
if (parsed.todos && Array.isArray(parsed.todos)) {
// Clear previous todos - we want the latest state
todos.length = 0;
for (const item of parsed.todos) {
if (item.content && item.status) {
// Check if this todo already exists (avoid duplicates)
if (!todos.some((t) => t.content === item.content)) {
todos.push({
content: item.content,
status: item.status,
});
}
todos.push({
content: item.content,
status: item.status as 'pending' | 'in_progress' | 'completed',
});
}
}
}
} catch {
// Ignore parse errors
}
} catch {
// Ignore parse errors
}
searchStart = markerIdx + 1;
}
// Also try to extract from markdown task lists
const markdownTodos = content.matchAll(/- \[([ xX])\] (.+)/g);
for (const match of markdownTodos) {
const isCompleted = match[1].toLowerCase() === 'x';
const content = match[2].trim();
if (!todos.some((t) => t.content === content)) {
todos.push({
content,
status: isCompleted ? 'completed' : 'pending',
});
// Also try to extract from markdown task lists as fallback
if (todos.length === 0) {
const markdownTodos = content.matchAll(/- \[([ xX])\] (.+)/g);
for (const match of markdownTodos) {
const isCompleted = match[1].toLowerCase() === 'x';
const todoContent = match[2].trim();
if (!todos.some((t) => t.content === todoContent)) {
todos.push({
content: todoContent,
status: isCompleted ? 'completed' : 'pending',
});
}
}
}

View File

@@ -153,3 +153,37 @@ export async function apiDeleteRaw(
): Promise<Response> {
return apiFetch(endpoint, 'DELETE', options);
}
/**
* Build an authenticated image URL for use in <img> tags or CSS background-image
* Adds authentication via query parameter since headers can't be set for image loads
*
* @param path - Image path
* @param projectPath - Project path
* @param version - Optional cache-busting version
* @returns Full URL with auth credentials
*/
export function getAuthenticatedImageUrl(
path: string,
projectPath: string,
version?: string | number
): string {
const serverUrl = getServerUrl();
const params = new URLSearchParams({
path,
projectPath,
});
if (version !== undefined) {
params.set('v', String(version));
}
// Add auth credential as query param (needed for image loads that can't set headers)
const apiKey = getApiKey();
if (apiKey) {
params.set('apiKey', apiKey);
}
// Note: Session token auth relies on cookies which are sent automatically by the browser
return `${serverUrl}/api/fs/image?${params.toString()}`;
}

View File

@@ -1,5 +1,4 @@
// Type definitions for Electron IPC API
import { createLogger } from '@automaker/utils/logger';
import type { SessionListItem, Message } from '@/types/electron';
import type { ClaudeUsageResponse } from '@/store/app-store';
import type {
@@ -11,10 +10,21 @@ import type {
IssueValidationResponse,
IssueValidationEvent,
StoredValidation,
ModelAlias,
AgentModel,
GitHubComment,
IssueCommentsResult,
ThinkingLevel,
Idea,
IdeaCategory,
IdeationSession,
IdeationMessage,
IdeationPrompt,
PromptCategory,
ProjectAnalysisResult,
AnalysisSuggestion,
StartSessionOptions,
CreateIdeaInput,
UpdateIdeaInput,
ConvertToFeatureOptions,
} from '@automaker/types';
import { getJSON, setJSON, removeItem } from './storage';
@@ -32,6 +42,104 @@ export type {
IssueCommentsResult,
};
// Re-export ideation types
export type {
Idea,
IdeaCategory,
IdeationSession,
IdeationMessage,
IdeationPrompt,
PromptCategory,
ProjectAnalysisResult,
AnalysisSuggestion,
StartSessionOptions,
CreateIdeaInput,
UpdateIdeaInput,
ConvertToFeatureOptions,
};
// Ideation API interface
export interface IdeationAPI {
// Session management
startSession: (
projectPath: string,
options?: StartSessionOptions
) => Promise<{ success: boolean; session?: IdeationSession; error?: string }>;
getSession: (
projectPath: string,
sessionId: string
) => Promise<{
success: boolean;
session?: IdeationSession;
messages?: IdeationMessage[];
error?: string;
}>;
sendMessage: (
sessionId: string,
message: string,
options?: { imagePaths?: string[]; model?: string }
) => Promise<{ success: boolean; error?: string }>;
stopSession: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
// Ideas CRUD
listIdeas: (projectPath: string) => Promise<{ success: boolean; ideas?: Idea[]; error?: string }>;
createIdea: (
projectPath: string,
idea: CreateIdeaInput
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
getIdea: (
projectPath: string,
ideaId: string
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
updateIdea: (
projectPath: string,
ideaId: string,
updates: UpdateIdeaInput
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
deleteIdea: (
projectPath: string,
ideaId: string
) => Promise<{ success: boolean; error?: string }>;
// Project analysis
analyzeProject: (
projectPath: string
) => Promise<{ success: boolean; analysis?: ProjectAnalysisResult; error?: string }>;
// Generate suggestions from a prompt
generateSuggestions: (
projectPath: string,
promptId: string,
category: IdeaCategory,
count?: number
) => Promise<{ success: boolean; suggestions?: AnalysisSuggestion[]; error?: string }>;
// Convert to feature
convertToFeature: (
projectPath: string,
ideaId: string,
options?: ConvertToFeatureOptions
) => Promise<{ success: boolean; feature?: any; featureId?: string; error?: string }>;
// Add suggestion directly to board as feature
addSuggestionToBoard: (
projectPath: string,
suggestion: AnalysisSuggestion
) => Promise<{ success: boolean; featureId?: string; error?: string }>;
// Get guided prompts (single source of truth from backend)
getPrompts: () => Promise<{
success: boolean;
prompts?: IdeationPrompt[];
categories?: PromptCategory[];
error?: string;
}>;
// Event subscriptions
onStream: (callback: (event: any) => void) => () => void;
onAnalysisEvent: (callback: (event: any) => void) => () => void;
}
export interface FileEntry {
name: string;
isDirectory: boolean;
@@ -96,8 +204,6 @@ import type {
ProviderStatus,
} from '@/types/electron';
const logger = createLogger('Electron');
// Import HTTP API client (ES module)
import { getHttpApiClient, getServerUrlSync } from './http-api-client';
@@ -208,8 +314,7 @@ export interface GitHubAPI {
validateIssue: (
projectPath: string,
issue: IssueValidationInput,
model?: ModelAlias,
thinkingLevel?: string
model?: AgentModel
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
/** Check validation status for an issue or all issues */
getValidationStatus: (
@@ -283,9 +388,7 @@ export type SuggestionType = 'features' | 'refactoring' | 'security' | 'performa
export interface SuggestionsAPI {
generate: (
projectPath: string,
suggestionType?: SuggestionType,
model?: string,
thinkingLevel?: ThinkingLevel
suggestionType?: SuggestionType
) => Promise<{ success: boolean; error?: string }>;
stop: () => Promise<{ success: boolean; error?: string }>;
status: () => Promise<{
@@ -565,10 +668,7 @@ export interface ElectronAPI {
isMac: boolean;
isLinux: boolean;
}>;
verifyClaudeAuth: (
authMethod?: 'cli' | 'api_key',
apiKey?: string
) => Promise<{
verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
@@ -582,19 +682,6 @@ export interface ElectronAPI {
user: string | null;
error?: string;
}>;
getCursorStatus?: () => Promise<{
success: boolean;
installed?: boolean;
version?: string | null;
path?: string | null;
auth?: {
authenticated: boolean;
method: string;
};
installCommand?: string;
loginCommand?: string;
error?: string;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void;
};
@@ -612,8 +699,7 @@ export interface ElectronAPI {
message: string,
workingDirectory?: string,
imagePaths?: string[],
model?: string,
thinkingLevel?: string
model?: string
) => Promise<{ success: boolean; error?: string }>;
getHistory: (sessionId: string) => Promise<{
success: boolean;
@@ -671,6 +757,7 @@ export interface ElectronAPI {
error?: string;
}>;
};
ideation?: IdeationAPI;
}
// Note: Window interface is declared in @/types/electron.d.ts
@@ -781,8 +868,8 @@ export const getCurrentApiMode = (): 'http' => {
// Debug helpers
if (typeof window !== 'undefined') {
(window as any).__checkApiMode = () => {
logger.info('Current API mode:', getCurrentApiMode());
logger.info('isElectron():', isElectron());
console.log('Current API mode:', getCurrentApiMode());
console.log('isElectron():', isElectron());
};
}
@@ -1023,7 +1110,7 @@ const getMockElectronAPI = (): ElectronAPI => {
// Store the image data in mock file system for testing
mockFileSystem[tempFilePath] = data;
logger.info('Mock saved image to temp:', tempFilePath);
console.log('[Mock] Saved image to temp:', tempFilePath);
return { success: true, path: tempFilePath };
},
@@ -1068,7 +1155,7 @@ const getMockElectronAPI = (): ElectronAPI => {
// Mock Claude API
claude: {
getUsage: async () => {
logger.info('Mock getting Claude usage');
console.log('[Mock] Getting Claude usage');
return {
sessionTokensUsed: 0,
sessionLimit: 0,
@@ -1150,10 +1237,7 @@ interface SetupAPI {
isMac: boolean;
isLinux: boolean;
}>;
verifyClaudeAuth: (
authMethod?: 'cli' | 'api_key',
apiKey?: string
) => Promise<{
verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
@@ -1175,7 +1259,7 @@ interface SetupAPI {
function createMockSetupAPI(): SetupAPI {
return {
getClaudeStatus: async () => {
logger.info('Mock Getting Claude status');
console.log('[Mock] Getting Claude status');
return {
success: true,
status: 'not_installed',
@@ -1192,7 +1276,7 @@ function createMockSetupAPI(): SetupAPI {
},
installClaude: async () => {
logger.info('Mock Installing Claude CLI');
console.log('[Mock] Installing Claude CLI');
// Simulate installation delay
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
@@ -1203,7 +1287,7 @@ function createMockSetupAPI(): SetupAPI {
},
authClaude: async () => {
logger.info('Mock Auth Claude CLI');
console.log('[Mock] Auth Claude CLI');
return {
success: true,
requiresManualAuth: true,
@@ -1212,13 +1296,13 @@ function createMockSetupAPI(): SetupAPI {
},
storeApiKey: async (provider: string, apiKey: string) => {
logger.info('Mock Storing API key for:', provider);
console.log('[Mock] Storing API key for:', provider);
// In mock mode, we just pretend to store it (it's already in the app store)
return { success: true };
},
getApiKeys: async () => {
logger.info('Mock Getting API keys');
console.log('[Mock] Getting API keys');
return {
success: true,
hasAnthropicKey: false,
@@ -1227,7 +1311,7 @@ function createMockSetupAPI(): SetupAPI {
},
deleteApiKey: async (provider: string) => {
logger.info('Mock Deleting API key for:', provider);
console.log('[Mock] Deleting API key for:', provider);
return { success: true, message: `API key for ${provider} deleted` };
},
@@ -1243,12 +1327,8 @@ function createMockSetupAPI(): SetupAPI {
};
},
verifyClaudeAuth: async (authMethod?: 'cli' | 'api_key', apiKey?: string) => {
logger.info(
'Mock verifying Claude auth with method:',
authMethod,
apiKey ? '(with key)' : ''
);
verifyClaudeAuth: async (authMethod?: 'cli' | 'api_key') => {
console.log('[Mock] Verifying Claude auth with method:', authMethod);
// Mock always returns not authenticated
return {
success: true,
@@ -1258,7 +1338,7 @@ function createMockSetupAPI(): SetupAPI {
},
getGhStatus: async () => {
logger.info('Mock Getting GitHub CLI status');
console.log('[Mock] Getting GitHub CLI status');
return {
success: true,
installed: false,
@@ -1285,7 +1365,7 @@ function createMockSetupAPI(): SetupAPI {
function createMockWorktreeAPI(): WorktreeAPI {
return {
mergeFeature: async (projectPath: string, featureId: string, options?: object) => {
logger.info('Mock Merging feature:', {
console.log('[Mock] Merging feature:', {
projectPath,
featureId,
options,
@@ -1294,7 +1374,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getInfo: async (projectPath: string, featureId: string) => {
logger.info('Mock Getting worktree info:', { projectPath, featureId });
console.log('[Mock] Getting worktree info:', { projectPath, featureId });
return {
success: true,
worktreePath: `/mock/worktrees/${featureId}`,
@@ -1304,7 +1384,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getStatus: async (projectPath: string, featureId: string) => {
logger.info('Mock Getting worktree status:', {
console.log('[Mock] Getting worktree status:', {
projectPath,
featureId,
});
@@ -1318,12 +1398,12 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
list: async (projectPath: string) => {
logger.info('Mock Listing worktrees:', { projectPath });
console.log('[Mock] Listing worktrees:', { projectPath });
return { success: true, worktrees: [] };
},
listAll: async (projectPath: string, includeDetails?: boolean) => {
logger.info('Mock Listing all worktrees:', {
console.log('[Mock] Listing all worktrees:', {
projectPath,
includeDetails,
});
@@ -1344,7 +1424,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
create: async (projectPath: string, branchName: string, baseBranch?: string) => {
logger.info('Mock Creating worktree:', {
console.log('[Mock] Creating worktree:', {
projectPath,
branchName,
baseBranch,
@@ -1360,7 +1440,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
delete: async (projectPath: string, worktreePath: string, deleteBranch?: boolean) => {
logger.info('Mock Deleting worktree:', {
console.log('[Mock] Deleting worktree:', {
projectPath,
worktreePath,
deleteBranch,
@@ -1375,7 +1455,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
commit: async (worktreePath: string, message: string) => {
logger.info('Mock Committing changes:', { worktreePath, message });
console.log('[Mock] Committing changes:', { worktreePath, message });
return {
success: true,
result: {
@@ -1388,7 +1468,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
push: async (worktreePath: string, force?: boolean) => {
logger.info('Mock Pushing worktree:', { worktreePath, force });
console.log('[Mock] Pushing worktree:', { worktreePath, force });
return {
success: true,
result: {
@@ -1400,7 +1480,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
createPR: async (worktreePath: string, options?: any) => {
logger.info('Mock Creating PR:', { worktreePath, options });
console.log('[Mock] Creating PR:', { worktreePath, options });
return {
success: true,
result: {
@@ -1415,7 +1495,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getDiffs: async (projectPath: string, featureId: string) => {
logger.info('Mock Getting file diffs:', { projectPath, featureId });
console.log('[Mock] Getting file diffs:', { projectPath, featureId });
return {
success: true,
diff: "diff --git a/src/feature.ts b/src/feature.ts\n+++ new file\n@@ -0,0 +1,10 @@\n+export function feature() {\n+ return 'hello';\n+}",
@@ -1428,7 +1508,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getFileDiff: async (projectPath: string, featureId: string, filePath: string) => {
logger.info('Mock Getting file diff:', {
console.log('[Mock] Getting file diff:', {
projectPath,
featureId,
filePath,
@@ -1441,7 +1521,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
pull: async (worktreePath: string) => {
logger.info('Mock Pulling latest changes for:', worktreePath);
console.log('[Mock] Pulling latest changes for:', worktreePath);
return {
success: true,
result: {
@@ -1453,7 +1533,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
checkoutBranch: async (worktreePath: string, branchName: string) => {
logger.info('Mock Creating and checking out branch:', {
console.log('[Mock] Creating and checking out branch:', {
worktreePath,
branchName,
});
@@ -1468,7 +1548,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
listBranches: async (worktreePath: string) => {
logger.info('Mock Listing branches for:', worktreePath);
console.log('[Mock] Listing branches for:', worktreePath);
return {
success: true,
result: {
@@ -1485,7 +1565,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
switchBranch: async (worktreePath: string, branchName: string) => {
logger.info('Mock Switching to branch:', { worktreePath, branchName });
console.log('[Mock] Switching to branch:', { worktreePath, branchName });
return {
success: true,
result: {
@@ -1497,7 +1577,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
openInEditor: async (worktreePath: string) => {
logger.info('Mock Opening in editor:', worktreePath);
console.log('[Mock] Opening in editor:', worktreePath);
return {
success: true,
result: {
@@ -1508,7 +1588,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getDefaultEditor: async () => {
logger.info('Mock Getting default editor');
console.log('[Mock] Getting default editor');
return {
success: true,
result: {
@@ -1519,7 +1599,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
initGit: async (projectPath: string) => {
logger.info('Mock Initializing git:', projectPath);
console.log('[Mock] Initializing git:', projectPath);
return {
success: true,
result: {
@@ -1530,7 +1610,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
startDevServer: async (projectPath: string, worktreePath: string) => {
logger.info('Mock Starting dev server:', { projectPath, worktreePath });
console.log('[Mock] Starting dev server:', { projectPath, worktreePath });
return {
success: true,
result: {
@@ -1543,7 +1623,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
stopDevServer: async (worktreePath: string) => {
logger.info('Mock Stopping dev server:', worktreePath);
console.log('[Mock] Stopping dev server:', worktreePath);
return {
success: true,
result: {
@@ -1554,7 +1634,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
listDevServers: async () => {
logger.info('Mock Listing dev servers');
console.log('[Mock] Listing dev servers');
return {
success: true,
result: {
@@ -1564,7 +1644,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
getPRInfo: async (worktreePath: string, branchName: string) => {
logger.info('Mock Getting PR info:', { worktreePath, branchName });
console.log('[Mock] Getting PR info:', { worktreePath, branchName });
return {
success: true,
result: {
@@ -1580,7 +1660,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
function createMockGitAPI(): GitAPI {
return {
getDiffs: async (projectPath: string) => {
logger.info('Mock Getting git diffs for project:', { projectPath });
console.log('[Mock] Getting git diffs for project:', { projectPath });
return {
success: true,
diff: "diff --git a/src/feature.ts b/src/feature.ts\n+++ new file\n@@ -0,0 +1,10 @@\n+export function feature() {\n+ return 'hello';\n+}",
@@ -1593,7 +1673,7 @@ function createMockGitAPI(): GitAPI {
},
getFileDiff: async (projectPath: string, filePath: string) => {
logger.info('Mock Getting git file diff:', { projectPath, filePath });
console.log('[Mock] Getting git file diff:', { projectPath, filePath });
return {
success: true,
diff: `diff --git a/${filePath} b/${filePath}\n+++ new file\n@@ -0,0 +1,5 @@\n+// New content`,
@@ -1617,7 +1697,7 @@ function createMockAutoModeAPI(): AutoModeAPI {
}
mockAutoModeRunning = true;
logger.info(`Mock auto mode started with maxConcurrency: ${maxConcurrency || 3}`);
console.log(`[Mock] Auto mode started with maxConcurrency: ${maxConcurrency || 3}`);
const featureId = 'auto-mode-0';
mockRunningFeatures.add(featureId);
@@ -1686,8 +1766,8 @@ function createMockAutoModeAPI(): AutoModeAPI {
};
}
logger.info(
`Mock running feature ${featureId} with useWorktrees: ${useWorktrees}, worktreePath: ${worktreePath}`
console.log(
`[Mock] Running feature ${featureId} with useWorktrees: ${useWorktrees}, worktreePath: ${worktreePath}`
);
mockRunningFeatures.add(featureId);
simulateAutoModeLoop(projectPath, featureId);
@@ -1854,7 +1934,7 @@ function createMockAutoModeAPI(): AutoModeAPI {
};
}
logger.info('Mock Follow-up feature:', {
console.log('[Mock] Follow-up feature:', {
featureId,
prompt,
imagePaths,
@@ -1871,7 +1951,7 @@ function createMockAutoModeAPI(): AutoModeAPI {
},
commitFeature: async (projectPath: string, featureId: string, worktreePath?: string) => {
logger.info('Mock Committing feature:', {
console.log('[Mock] Committing feature:', {
projectPath,
featureId,
worktreePath,
@@ -1916,7 +1996,7 @@ function createMockAutoModeAPI(): AutoModeAPI {
editedPlan?: string,
feedback?: string
) => {
logger.info('Mock Plan approval:', {
console.log('[Mock] Plan approval:', {
projectPath,
featureId,
approved,
@@ -2068,12 +2148,7 @@ let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
function createMockSuggestionsAPI(): SuggestionsAPI {
return {
generate: async (
projectPath: string,
suggestionType: SuggestionType = 'features',
model?: string,
thinkingLevel?: ThinkingLevel
) => {
generate: async (projectPath: string, suggestionType: SuggestionType = 'features') => {
if (mockSuggestionsRunning) {
return {
success: false,
@@ -2082,11 +2157,7 @@ function createMockSuggestionsAPI(): SuggestionsAPI {
}
mockSuggestionsRunning = true;
logger.info(
`Mock generating ${suggestionType} suggestions for: ${projectPath}` +
(model ? ` with model: ${model}` : '') +
(thinkingLevel ? ` thinkingLevel: ${thinkingLevel}` : '')
);
console.log(`[Mock] Generating ${suggestionType} suggestions for: ${projectPath}`);
// Simulate async suggestion generation
simulateSuggestionsGeneration(suggestionType);
@@ -2310,8 +2381,8 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
}
mockSpecRegenerationRunning = true;
logger.info(
`Mock creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
console.log(
`[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
);
// Simulate async spec creation
@@ -2335,8 +2406,8 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
}
mockSpecRegenerationRunning = true;
logger.info(
`Mock regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
console.log(
`[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
);
// Simulate async spec regeneration
@@ -2354,8 +2425,8 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
}
mockSpecRegenerationRunning = true;
logger.info(
`Mock generating features from existing spec for: ${projectPath}, maxFeatures: ${maxFeatures}`
console.log(
`[Mock] Generating features from existing spec for: ${projectPath}, maxFeatures: ${maxFeatures}`
);
// Simulate async feature generation
@@ -2620,7 +2691,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
// Store features in mock file system using features/{id}/feature.json pattern
return {
getAll: async (projectPath: string) => {
logger.info('Mock Getting all features for:', projectPath);
console.log('[Mock] Getting all features for:', projectPath);
// Check if test has set mock features via global variable
const testFeatures = (window as any).__mockFeatures;
@@ -2645,7 +2716,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
features.push(feature);
}
} catch (error) {
logger.error('Mock Failed to parse feature:', error);
console.error('[Mock] Failed to parse feature:', error);
}
}
@@ -2658,7 +2729,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
},
get: async (projectPath: string, featureId: string) => {
logger.info('Mock Getting feature:', { projectPath, featureId });
console.log('[Mock] Getting feature:', { projectPath, featureId });
const featurePath = `${projectPath}/.automaker/features/${featureId}/feature.json`;
const content = mockFileSystem[featurePath];
if (content) {
@@ -2668,7 +2739,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
},
create: async (projectPath: string, feature: Feature) => {
logger.info('Mock Creating feature:', {
console.log('[Mock] Creating feature:', {
projectPath,
featureId: feature.id,
});
@@ -2678,7 +2749,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
},
update: async (projectPath: string, featureId: string, updates: Partial<Feature>) => {
logger.info('Mock Updating feature:', {
console.log('[Mock] Updating feature:', {
projectPath,
featureId,
updates,
@@ -2694,7 +2765,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
},
delete: async (projectPath: string, featureId: string) => {
logger.info('Mock Deleting feature:', { projectPath, featureId });
console.log('[Mock] Deleting feature:', { projectPath, featureId });
const featurePath = `${projectPath}/.automaker/features/${featureId}/feature.json`;
delete mockFileSystem[featurePath];
// Also delete agent-output.md if it exists
@@ -2704,14 +2775,14 @@ function createMockFeaturesAPI(): FeaturesAPI {
},
getAgentOutput: async (projectPath: string, featureId: string) => {
logger.info('Mock Getting agent output:', { projectPath, featureId });
console.log('[Mock] Getting agent output:', { projectPath, featureId });
const agentOutputPath = `${projectPath}/.automaker/features/${featureId}/agent-output.md`;
const content = mockFileSystem[agentOutputPath];
return { success: true, content: content || null };
},
generateTitle: async (description: string) => {
logger.info('Mock Generating title for:', description.substring(0, 50));
console.log('[Mock] Generating title for:', description.substring(0, 50));
// Mock title generation - just take first few words
const words = description.split(/\s+/).slice(0, 6).join(' ');
const title = words.length > 40 ? words.substring(0, 40) + '...' : words;
@@ -2724,7 +2795,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
function createMockRunningAgentsAPI(): RunningAgentsAPI {
return {
getAll: async () => {
logger.info('Mock Getting all running agents');
console.log('[Mock] Getting all running agents');
// Return running agents from mock auto mode state
const runningAgents: RunningAgent[] = Array.from(mockRunningFeatures).map((featureId) => ({
featureId,
@@ -2749,7 +2820,7 @@ let mockValidationCallbacks: ((event: IssueValidationEvent) => void)[] = [];
function createMockGitHubAPI(): GitHubAPI {
return {
checkRemote: async (projectPath: string) => {
logger.info('Mock Checking GitHub remote for:', projectPath);
console.log('[Mock] Checking GitHub remote for:', projectPath);
return {
success: true,
hasGitHubRemote: false,
@@ -2759,7 +2830,7 @@ function createMockGitHubAPI(): GitHubAPI {
};
},
listIssues: async (projectPath: string) => {
logger.info('Mock Listing GitHub issues for:', projectPath);
console.log('[Mock] Listing GitHub issues for:', projectPath);
return {
success: true,
openIssues: [],
@@ -2767,15 +2838,15 @@ function createMockGitHubAPI(): GitHubAPI {
};
},
listPRs: async (projectPath: string) => {
logger.info('Mock Listing GitHub PRs for:', projectPath);
console.log('[Mock] Listing GitHub PRs for:', projectPath);
return {
success: true,
openPRs: [],
mergedPRs: [],
};
},
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: ModelAlias) => {
logger.info('Mock Starting async validation:', { projectPath, issue, model });
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: AgentModel) => {
console.log('[Mock] Starting async validation:', { projectPath, issue, model });
// Simulate async validation in background
setTimeout(() => {
@@ -2816,7 +2887,7 @@ function createMockGitHubAPI(): GitHubAPI {
};
},
getValidationStatus: async (projectPath: string, issueNumber?: number) => {
logger.info('Mock Getting validation status:', { projectPath, issueNumber });
console.log('[Mock] Getting validation status:', { projectPath, issueNumber });
return {
success: true,
isRunning: false,
@@ -2824,21 +2895,21 @@ function createMockGitHubAPI(): GitHubAPI {
};
},
stopValidation: async (projectPath: string, issueNumber: number) => {
logger.info('Mock Stopping validation:', { projectPath, issueNumber });
console.log('[Mock] Stopping validation:', { projectPath, issueNumber });
return {
success: true,
message: `Validation for issue #${issueNumber} stopped`,
};
},
getValidations: async (projectPath: string, issueNumber?: number) => {
logger.info('Mock Getting validations:', { projectPath, issueNumber });
console.log('[Mock] Getting validations:', { projectPath, issueNumber });
return {
success: true,
validations: [],
};
},
markValidationViewed: async (projectPath: string, issueNumber: number) => {
logger.info('Mock Marking validation as viewed:', { projectPath, issueNumber });
console.log('[Mock] Marking validation as viewed:', { projectPath, issueNumber });
return {
success: true,
};
@@ -2850,7 +2921,7 @@ function createMockGitHubAPI(): GitHubAPI {
};
},
getIssueComments: async (projectPath: string, issueNumber: number, cursor?: string) => {
logger.info('Mock Getting issue comments:', { projectPath, issueNumber, cursor });
console.log('[Mock] Getting issue comments:', { projectPath, issueNumber, cursor });
return {
success: true,
comments: [],

View File

@@ -27,6 +27,13 @@ import type {
GitHubPR,
IssueValidationInput,
IssueValidationEvent,
IdeationAPI,
IdeaCategory,
AnalysisSuggestion,
StartSessionOptions,
CreateIdeaInput,
UpdateIdeaInput,
ConvertToFeatureOptions,
} from './electron';
import type { Message, SessionListItem } from '@/types/electron';
import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
@@ -371,7 +378,9 @@ type EventType =
| 'suggestions:event'
| 'spec-regeneration:event'
| 'issue-validation:event'
| 'backlog-plan:event';
| 'backlog-plan:event'
| 'ideation:stream'
| 'ideation:analysis';
type EventCallback = (payload: unknown) => void;
@@ -1577,8 +1586,6 @@ export class HttpApiClient implements ElectronAPI {
headers?: Record<string, string>;
enabled?: boolean;
}>;
mcpAutoApproveTools?: boolean;
mcpUnrestrictedTools?: boolean;
};
error?: string;
}> => this.get('/api/settings/global'),
@@ -1779,6 +1786,66 @@ export class HttpApiClient implements ElectronAPI {
},
};
// Ideation API - brainstorming and idea management
ideation: IdeationAPI = {
startSession: (projectPath: string, options?: StartSessionOptions) =>
this.post('/api/ideation/session/start', { projectPath, options }),
getSession: (projectPath: string, sessionId: string) =>
this.post('/api/ideation/session/get', { projectPath, sessionId }),
sendMessage: (
sessionId: string,
message: string,
options?: { imagePaths?: string[]; model?: string }
) => this.post('/api/ideation/session/message', { sessionId, message, options }),
stopSession: (sessionId: string) => this.post('/api/ideation/session/stop', { sessionId }),
listIdeas: (projectPath: string) => this.post('/api/ideation/ideas/list', { projectPath }),
createIdea: (projectPath: string, idea: CreateIdeaInput) =>
this.post('/api/ideation/ideas/create', { projectPath, idea }),
getIdea: (projectPath: string, ideaId: string) =>
this.post('/api/ideation/ideas/get', { projectPath, ideaId }),
updateIdea: (projectPath: string, ideaId: string, updates: UpdateIdeaInput) =>
this.post('/api/ideation/ideas/update', { projectPath, ideaId, updates }),
deleteIdea: (projectPath: string, ideaId: string) =>
this.post('/api/ideation/ideas/delete', { projectPath, ideaId }),
analyzeProject: (projectPath: string) => this.post('/api/ideation/analyze', { projectPath }),
generateSuggestions: (
projectPath: string,
promptId: string,
category: IdeaCategory,
count?: number
) =>
this.post('/api/ideation/suggestions/generate', { projectPath, promptId, category, count }),
convertToFeature: (projectPath: string, ideaId: string, options?: ConvertToFeatureOptions) =>
this.post('/api/ideation/convert', { projectPath, ideaId, ...options }),
addSuggestionToBoard: (
projectPath: string,
suggestion: AnalysisSuggestion
): Promise<{ success: boolean; featureId?: string; error?: string }> =>
this.post('/api/ideation/add-suggestion', { projectPath, suggestion }),
getPrompts: () => this.get('/api/ideation/prompts'),
onStream: (callback: (event: any) => void): (() => void) => {
return this.subscribeToEvent('ideation:stream', callback as EventCallback);
},
onAnalysisEvent: (callback: (event: any) => void): (() => void) => {
return this.subscribeToEvent('ideation:analysis', callback as EventCallback);
},
};
// MCP API - Test MCP server connections and list tools
// SECURITY: Only accepts serverId, not arbitrary serverConfig, to prevent
// drive-by command execution attacks. Servers must be saved first.

View File

@@ -1196,6 +1196,53 @@ function mergeConsecutiveEntries(entries: LogEntry[]): LogEntry[] {
return merged;
}
/**
* Extracts summary content from raw log output
* Returns the summary text if found, or null if no summary exists
*/
export function extractSummary(rawOutput: string): string | null {
if (!rawOutput || !rawOutput.trim()) {
return null;
}
// Try to find <summary> tags first (preferred format)
const summaryTagMatch = rawOutput.match(/<summary>([\s\S]*?)<\/summary>/);
if (summaryTagMatch) {
return summaryTagMatch[1].trim();
}
// Try to find markdown ## Summary section
const summaryHeaderMatch = rawOutput.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s+|$)/m);
if (summaryHeaderMatch) {
return summaryHeaderMatch[1].trim();
}
// Try other summary formats (Feature, Changes, Implementation)
const otherHeaderMatch = rawOutput.match(
/^##\s+(Feature|Changes|Implementation)\s*\n([\s\S]*?)(?=\n##\s+|$)/m
);
if (otherHeaderMatch) {
return `## ${otherHeaderMatch[1]}\n${otherHeaderMatch[2].trim()}`;
}
// Try to find summary introduction lines
const introMatch = rawOutput.match(
/(^|\n)(All tasks completed[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/
);
if (introMatch) {
return introMatch[2].trim();
}
const completionMatch = rawOutput.match(
/(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/
);
if (completionMatch) {
return completionMatch[2].trim();
}
return null;
}
/**
* Gets the color classes for a log entry type
*/