mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
Feature: worktree view customization and stability fixes (#805)
* Changes from feature/worktree-view-customization * Feature: Git sync, set-tracking, and push divergence handling (#796) * Add quick-add feature with improved workflows (#802) * Changes from feature/quick-add * feat: Clarify system prompt and improve error handling across services. Address PR Feedback * feat: Improve PR description parsing and refactor event handling * feat: Add context options to pipeline orchestrator initialization * fix: Deduplicate React and handle CJS interop for use-sync-external-store Resolve "Cannot read properties of null (reading 'useState')" errors by deduplicating React/react-dom and ensuring use-sync-external-store is bundled together with React to prevent CJS packages from resolving to different React instances. * Changes from feature/worktree-view-customization * refactor: Remove unused worktree swap and highlight props * refactor: Consolidate feature completion logic and improve thinking level defaults * feat: Increase max turn limit to 10000 - Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts - Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts - Update UI clamping logic from 2000 to 10000 in app-store.ts - Update fallback values from 1000 to 10000 in use-settings-sync.ts - Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS - Update documentation to reflect new range: 1-10000 Allows agents to perform up to 10000 turns for complex feature execution. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat: Add model resolution, improve session handling, and enhance UI stability * refactor: Remove unused sync and tracking branch props from worktree components * feat: Add PR number update functionality to worktrees. Address pr feedback * feat: Optimize Gemini CLI startup and add tool result tracking * refactor: Improve error handling and simplify worktree task cleanup --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ export type {
|
||||
|
||||
const logger = createLogger('AgentExecutor');
|
||||
|
||||
const DEFAULT_MAX_TURNS = 1000;
|
||||
const DEFAULT_MAX_TURNS = 10000;
|
||||
|
||||
export class AgentExecutor {
|
||||
private static readonly WRITE_DEBOUNCE_MS = 500;
|
||||
|
||||
@@ -487,7 +487,19 @@ export class AgentService {
|
||||
Object.keys(customSubagents).length > 0;
|
||||
|
||||
// Base tools that match the provider's default set
|
||||
const baseTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||
const baseTools = [
|
||||
'Read',
|
||||
'Write',
|
||||
'Edit',
|
||||
'MultiEdit',
|
||||
'Glob',
|
||||
'Grep',
|
||||
'LS',
|
||||
'Bash',
|
||||
'WebSearch',
|
||||
'WebFetch',
|
||||
'TodoWrite',
|
||||
];
|
||||
|
||||
if (allowedTools) {
|
||||
allowedTools = [...allowedTools]; // Create a copy to avoid mutating SDK options
|
||||
@@ -572,6 +584,7 @@ export class AgentService {
|
||||
let currentAssistantMessage: Message | null = null;
|
||||
let responseText = '';
|
||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
||||
const toolNamesById = new Map<string, string>();
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Capture SDK session ID from any message and persist it.
|
||||
@@ -616,11 +629,50 @@ export class AgentService {
|
||||
input: block.input,
|
||||
};
|
||||
toolUses.push(toolUse);
|
||||
if (block.tool_use_id) {
|
||||
toolNamesById.set(block.tool_use_id, toolUse.name);
|
||||
}
|
||||
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'tool_use',
|
||||
tool: toolUse,
|
||||
});
|
||||
} else if (block.type === 'tool_result') {
|
||||
const toolUseId = block.tool_use_id;
|
||||
const toolName = toolUseId ? toolNamesById.get(toolUseId) : undefined;
|
||||
|
||||
// Normalize block.content to a string for the emitted event
|
||||
const rawContent: unknown = block.content;
|
||||
let contentString: string;
|
||||
if (typeof rawContent === 'string') {
|
||||
contentString = rawContent;
|
||||
} else if (Array.isArray(rawContent)) {
|
||||
// Extract text from content blocks (TextBlock, ImageBlock, etc.)
|
||||
contentString = rawContent
|
||||
.map((part: { text?: string; type?: string }) => {
|
||||
if (typeof part === 'string') return part;
|
||||
if (part.text) return part.text;
|
||||
// For non-text blocks (e.g., images), represent as type indicator
|
||||
if (part.type) return `[${part.type}]`;
|
||||
return JSON.stringify(part);
|
||||
})
|
||||
.join('\n');
|
||||
} else if (rawContent !== undefined && rawContent !== null) {
|
||||
contentString = JSON.stringify(rawContent);
|
||||
} else {
|
||||
contentString = '';
|
||||
}
|
||||
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'tool_result',
|
||||
tool: {
|
||||
name: toolName || 'unknown',
|
||||
input: {
|
||||
toolUseId,
|
||||
content: contentString,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +767,7 @@ export class AutoModeServiceFacade {
|
||||
featureId,
|
||||
featureName: feature?.title,
|
||||
branchName: feature?.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: allPassed,
|
||||
message: allPassed
|
||||
? 'All verification checks passed'
|
||||
@@ -829,6 +830,7 @@ export class AutoModeServiceFacade {
|
||||
featureId,
|
||||
featureName: feature?.title,
|
||||
branchName: feature?.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: `Changes committed: ${hash.trim().substring(0, 8)}`,
|
||||
projectPath: this.projectPath,
|
||||
|
||||
@@ -60,6 +60,7 @@ interface AutoModeEventPayload {
|
||||
featureId?: string;
|
||||
featureName?: string;
|
||||
passes?: boolean;
|
||||
executionMode?: 'auto' | 'manual';
|
||||
message?: string;
|
||||
error?: string;
|
||||
errorType?: string;
|
||||
@@ -99,6 +100,18 @@ function isFeatureStatusChangedPayload(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature completed event payload structure
|
||||
*/
|
||||
interface FeatureCompletedPayload {
|
||||
featureId: string;
|
||||
featureName?: string;
|
||||
projectPath: string;
|
||||
passes?: boolean;
|
||||
message?: string;
|
||||
executionMode?: 'auto' | 'manual';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Hook Service
|
||||
*
|
||||
@@ -150,6 +163,8 @@ export class EventHookService {
|
||||
this.handleAutoModeEvent(payload as AutoModeEventPayload);
|
||||
} else if (type === 'feature:created') {
|
||||
this.handleFeatureCreatedEvent(payload as FeatureCreatedPayload);
|
||||
} else if (type === 'feature:completed') {
|
||||
this.handleFeatureCompletedEvent(payload as FeatureCompletedPayload);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -187,6 +202,9 @@ export class EventHookService {
|
||||
|
||||
switch (payload.type) {
|
||||
case 'auto_mode_feature_complete':
|
||||
// Only map explicit auto-mode completion events.
|
||||
// Manual feature completions are emitted as feature:completed.
|
||||
if (payload.executionMode !== 'auto') return;
|
||||
trigger = payload.passes ? 'feature_success' : 'feature_error';
|
||||
// Track this feature so feature_status_changed doesn't double-fire hooks
|
||||
if (payload.featureId) {
|
||||
@@ -248,6 +266,46 @@ export class EventHookService {
|
||||
await this.executeHooksForTrigger(trigger, context, { passes: payload.passes });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle feature:completed events and trigger matching hooks
|
||||
*/
|
||||
private async handleFeatureCompletedEvent(payload: FeatureCompletedPayload): Promise<void> {
|
||||
if (!payload.featureId || !payload.projectPath) return;
|
||||
|
||||
// Mark as handled to prevent duplicate firing if feature_status_changed also fires
|
||||
this.markFeatureHandled(payload.featureId);
|
||||
|
||||
const passes = payload.passes ?? true;
|
||||
const trigger: EventHookTrigger = passes ? 'feature_success' : 'feature_error';
|
||||
|
||||
// Load feature name if we have featureId but no featureName
|
||||
let featureName: string | undefined = undefined;
|
||||
if (payload.projectPath && this.featureLoader) {
|
||||
try {
|
||||
const feature = await this.featureLoader.get(payload.projectPath, payload.featureId);
|
||||
if (feature?.title) {
|
||||
featureName = feature.title;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to load feature ${payload.featureId} for event hook:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const isErrorTrigger = trigger === 'feature_error';
|
||||
const context: HookContext = {
|
||||
featureId: payload.featureId,
|
||||
featureName: featureName || payload.featureName,
|
||||
projectPath: payload.projectPath,
|
||||
projectName: this.extractProjectName(payload.projectPath),
|
||||
error: isErrorTrigger ? payload.message : undefined,
|
||||
errorType: undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
eventType: trigger,
|
||||
};
|
||||
|
||||
await this.executeHooksForTrigger(trigger, context, { passes });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle feature:created events and trigger matching hooks
|
||||
*/
|
||||
|
||||
@@ -457,6 +457,7 @@ Please continue from where you left off and complete all remaining tasks. Use th
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: completionMessage,
|
||||
projectPath,
|
||||
@@ -473,6 +474,7 @@ Please continue from where you left off and complete all remaining tasks. Use th
|
||||
featureId,
|
||||
featureName: feature?.title,
|
||||
branchName: feature?.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: false,
|
||||
message: 'Feature stopped by user',
|
||||
projectPath,
|
||||
@@ -502,6 +504,22 @@ Please continue from where you left off and complete all remaining tasks. Use th
|
||||
async stopFeature(featureId: string): Promise<boolean> {
|
||||
const running = this.concurrencyManager.getRunningFeature(featureId);
|
||||
if (!running) return false;
|
||||
const { projectPath } = running;
|
||||
|
||||
// Immediately update feature status to 'interrupted' so the UI reflects
|
||||
// the stop right away. CLI-based providers can take seconds to terminate
|
||||
// their subprocess after the abort signal fires, leaving the feature stuck
|
||||
// in 'in_progress' on the Kanban board until the executeFeature catch block
|
||||
// eventually runs. By persisting and emitting the status change here, the
|
||||
// board updates immediately regardless of how long the subprocess takes to stop.
|
||||
try {
|
||||
await this.updateFeatureStatusFn(projectPath, featureId, 'interrupted');
|
||||
} catch (err) {
|
||||
// Non-fatal: the abort still proceeds and executeFeature's catch block
|
||||
// will attempt the same update once the subprocess terminates.
|
||||
logger.warn(`stopFeature: failed to immediately update status for ${featureId}:`, err);
|
||||
}
|
||||
|
||||
running.abortController.abort();
|
||||
this.releaseRunningFeature(featureId, { force: true });
|
||||
return true;
|
||||
|
||||
@@ -243,6 +243,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: 'Pipeline step no longer exists',
|
||||
projectPath,
|
||||
@@ -292,6 +293,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: 'Pipeline completed (remaining steps excluded)',
|
||||
projectPath,
|
||||
@@ -317,6 +319,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: 'Pipeline completed (all steps excluded)',
|
||||
projectPath,
|
||||
@@ -401,6 +404,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: 'Pipeline resumed successfully',
|
||||
projectPath,
|
||||
@@ -414,6 +418,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName: feature.branchName ?? null,
|
||||
executionMode: 'auto',
|
||||
passes: false,
|
||||
message: 'Pipeline stopped by user',
|
||||
projectPath,
|
||||
@@ -580,6 +585,7 @@ export class PipelineOrchestrator {
|
||||
featureId,
|
||||
featureName: feature.title,
|
||||
branchName,
|
||||
executionMode: 'auto',
|
||||
passes: true,
|
||||
message: 'Pipeline completed and merged',
|
||||
projectPath,
|
||||
|
||||
@@ -8,13 +8,10 @@
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { execGitCommand } from '@automaker/git-utils';
|
||||
import type { EventEmitter } from '../lib/events.js';
|
||||
import type { SettingsService } from './settings-service.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Get the list of remote names that have a branch matching the given branch name.
|
||||
*
|
||||
@@ -41,10 +38,9 @@ export async function getRemotesWithBranch(
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: remoteRefsOutput } = await execFileAsync(
|
||||
'git',
|
||||
const remoteRefsOutput = await execGitCommand(
|
||||
['for-each-ref', '--format=%(refname:short)', `refs/remotes/*/${currentBranch}`],
|
||||
{ cwd: worktreePath }
|
||||
worktreePath
|
||||
);
|
||||
|
||||
if (!remoteRefsOutput.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user