fix: adress pr comments

This commit is contained in:
Shirone
2026-01-24 18:45:05 +01:00
parent cec5f91a86
commit b1060c6a11
5 changed files with 244 additions and 184 deletions

View File

@@ -274,49 +274,60 @@ function detectSpecFallback(text: string): boolean {
* 4. **Problem**: or **Problem Statement**: section (spec/full modes)
* 5. **Solution**: section as fallback
*
* Note: Uses last match for each pattern to avoid stale summaries
* when agent output accumulates across multiple runs.
*
* @param text - The text content to extract summary from
* @returns The extracted summary string, or null if no summary found
*/
function extractSummary(text: string): string | null {
// Check for explicit <summary> tags first
const summaryMatch = text.match(/<summary>([\s\S]*?)<\/summary>/);
// Helper to truncate content to first paragraph with max length
const truncate = (content: string, maxLength: number): string => {
const firstPara = content.split(/\n\n/)[0];
return firstPara.length > maxLength ? `${firstPara.substring(0, maxLength)}...` : firstPara;
};
// Helper to get last match from matchAll results
const getLastMatch = (matches: IterableIterator<RegExpMatchArray>): RegExpMatchArray | null => {
const arr = [...matches];
return arr.length > 0 ? arr[arr.length - 1] : null;
};
// Check for explicit <summary> tags first (use last match to avoid stale summaries)
const summaryMatches = text.matchAll(/<summary>([\s\S]*?)<\/summary>/g);
const summaryMatch = getLastMatch(summaryMatches);
if (summaryMatch) {
return summaryMatch[1].trim();
}
// Check for ## Summary section
const sectionMatch = text.match(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##|\n\*\*|$)/i);
// Check for ## Summary section (use last match)
const sectionMatches = text.matchAll(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##|\n\*\*|$)/gi);
const sectionMatch = getLastMatch(sectionMatches);
if (sectionMatch) {
const content = sectionMatch[1].trim();
// Take first paragraph or up to 500 chars
const firstPara = content.split(/\n\n/)[0];
return firstPara.length > 500 ? firstPara.substring(0, 500) + '...' : firstPara;
return truncate(sectionMatch[1].trim(), 500);
}
// Check for **Goal**: section (lite mode)
const goalMatch = text.match(/\*\*Goal\*\*:\s*(.+?)(?:\n|$)/i);
// Check for **Goal**: section (lite mode, use last match)
const goalMatches = text.matchAll(/\*\*Goal\*\*:\s*(.+?)(?:\n|$)/gi);
const goalMatch = getLastMatch(goalMatches);
if (goalMatch) {
return goalMatch[1].trim();
}
// Check for **Problem**: or **Problem Statement**: section (spec/full modes)
const problemMatch = text.match(
/\*\*Problem(?:\s*Statement)?\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/i
// Check for **Problem**: or **Problem Statement**: section (spec/full modes, use last match)
const problemMatches = text.matchAll(
/\*\*Problem(?:\s*Statement)?\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi
);
const problemMatch = getLastMatch(problemMatches);
if (problemMatch) {
const content = problemMatch[1].trim();
// Take first paragraph or up to 500 chars
const firstPara = content.split(/\n\n/)[0];
return firstPara.length > 500 ? firstPara.substring(0, 500) + '...' : firstPara;
return truncate(problemMatch[1].trim(), 500);
}
// Check for **Solution**: section as fallback
const solutionMatch = text.match(/\*\*Solution\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/i);
// Check for **Solution**: section as fallback (use last match)
const solutionMatches = text.matchAll(/\*\*Solution\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi);
const solutionMatch = getLastMatch(solutionMatches);
if (solutionMatch) {
const content = solutionMatch[1].trim();
// Take first paragraph or up to 300 chars
const firstPara = content.split(/\n\n/)[0];
return firstPara.length > 300 ? firstPara.substring(0, 300) + '...' : firstPara;
return truncate(solutionMatch[1].trim(), 300);
}
return null;
@@ -4008,6 +4019,168 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
);
}, STREAM_HEARTBEAT_MS);
// RECOVERY PATH: If we have an approved plan with persisted tasks, skip spec generation
// and directly execute the remaining tasks
if (existingApprovedPlan && persistedTasks && persistedTasks.length > 0) {
logger.info(
`Recovery: Resuming task execution for feature ${featureId} with ${persistedTasks.length} tasks`
);
// Get customized prompts for task execution
const taskPrompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
const approvedPlanContent = existingApprovedPlan.content || '';
// Execute each task with a separate agent
for (let taskIndex = 0; taskIndex < persistedTasks.length; taskIndex++) {
const task = persistedTasks[taskIndex];
// Skip tasks that are already completed
if (task.status === 'completed') {
logger.info(`Skipping already completed task ${task.id}`);
continue;
}
// Check for abort
if (abortController.signal.aborted) {
throw new Error('Feature execution aborted');
}
// Mark task as in_progress immediately (even without TASK_START marker)
await this.updateTaskStatus(projectPath, featureId, task.id, 'in_progress');
// Emit task started
logger.info(`Starting task ${task.id}: ${task.description}`);
this.emitAutoModeEvent('auto_mode_task_started', {
featureId,
projectPath,
branchName,
taskId: task.id,
taskDescription: task.description,
taskIndex,
tasksTotal: persistedTasks.length,
});
// Update planSpec with current task
await this.updateFeaturePlanSpec(projectPath, featureId, {
currentTaskId: task.id,
});
// Build focused prompt for this specific task
const taskPrompt = this.buildTaskPrompt(
task,
persistedTasks,
taskIndex,
approvedPlanContent,
taskPrompts.taskExecution.taskPromptTemplate,
undefined
);
// Execute task with dedicated agent
const taskStream = provider.executeQuery({
prompt: taskPrompt,
model: bareModel,
maxTurns: Math.min(maxTurns || 100, 50),
cwd: workDir,
allowedTools: allowedTools,
abortController,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
credentials,
claudeCompatibleProvider,
});
let taskOutput = '';
let taskCompleteDetected = false;
// Process task stream
for await (const msg of taskStream) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
const text = block.text || '';
taskOutput += text;
responseText += text;
this.emitAutoModeEvent('auto_mode_progress', {
featureId,
branchName,
content: text,
});
scheduleWrite();
// Detect [TASK_COMPLETE] marker
if (!taskCompleteDetected) {
const completeTaskId = detectTaskCompleteMarker(taskOutput);
if (completeTaskId) {
taskCompleteDetected = true;
logger.info(`[TASK_COMPLETE] detected for ${completeTaskId}`);
await this.updateTaskStatus(
projectPath,
featureId,
completeTaskId,
'completed'
);
}
}
} else if (block.type === 'tool_use') {
this.emitAutoModeEvent('auto_mode_tool', {
featureId,
branchName,
tool: block.name,
input: block.input,
});
}
}
} else if (msg.type === 'error') {
throw new Error(msg.error || `Error during task ${task.id}`);
} else if (msg.type === 'result' && msg.subtype === 'success') {
taskOutput += msg.result || '';
responseText += msg.result || '';
}
}
// If no [TASK_COMPLETE] marker was detected, still mark as completed
if (!taskCompleteDetected) {
await this.updateTaskStatus(projectPath, featureId, task.id, 'completed');
}
// Emit task completed
logger.info(`Task ${task.id} completed for feature ${featureId}`);
this.emitAutoModeEvent('auto_mode_task_complete', {
featureId,
projectPath,
branchName,
taskId: task.id,
tasksCompleted: taskIndex + 1,
tasksTotal: persistedTasks.length,
});
// Update planSpec with progress
await this.updateFeaturePlanSpec(projectPath, featureId, {
tasksCompleted: taskIndex + 1,
});
}
logger.info(`Recovery: All tasks completed for feature ${featureId}`);
// Extract and save final summary
const summary = extractSummary(responseText);
if (summary) {
await this.saveFeatureSummary(projectPath, featureId, summary);
this.emitAutoModeEvent('auto_mode_summary', {
featureId,
projectPath,
summary,
});
}
// Final write and cleanup
clearInterval(streamHeartbeat);
if (writeTimeout) {
clearTimeout(writeTimeout);
}
await writeToFile();
return;
}
// Wrap stream processing in try/finally to ensure timeout cleanup on any error/abort
try {
streamLoop: for await (const msg of stream) {
@@ -4359,6 +4532,9 @@ After generating the revised spec, output:
throw new Error('Feature execution aborted');
}
// Mark task as in_progress immediately (even without TASK_START marker)
await this.updateTaskStatus(projectPath, featureId, task.id, 'in_progress');
// Emit task started
logger.info(`Starting task ${task.id}: ${task.description}`);
this.emitAutoModeEvent('auto_mode_task_started', {

View File

@@ -179,9 +179,6 @@ export function AddFeatureDialog({
// Model selection state
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>({ model: 'claude-opus' });
// All models support planning mode via marker-based instructions in prompts
const modelSupportsPlanningMode = true;
// Planning mode state
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
@@ -562,41 +559,13 @@ export function AddFeatureDialog({
<div className="grid gap-3 grid-cols-2">
<div className="space-y-1.5">
<Label
className={cn(
'text-xs text-muted-foreground',
!modelSupportsPlanningMode && 'opacity-50'
)}
>
Planning
</Label>
{modelSupportsPlanningMode ? (
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="add-feature-planning"
compact
/>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="add-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Label className="text-xs text-muted-foreground">Planning</Label>
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="add-feature-planning"
compact
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Options</Label>
@@ -620,20 +589,14 @@ export function AddFeatureDialog({
id="add-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
}
disabled={planningMode === 'skip' || planningMode === 'lite'}
data-testid="add-feature-require-approval-checkbox"
/>
<Label
htmlFor="add-feature-require-approval"
className={cn(
'text-xs font-normal',
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
planningMode === 'skip' || planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}

View File

@@ -119,9 +119,6 @@ export function EditFeatureDialog({
reasoningEffort: feature?.reasoningEffort || 'none',
}));
// All models support planning mode via marker-based instructions in prompts
const modelSupportsPlanningMode = true;
// Track the source of description changes for history
const [descriptionChangeSource, setDescriptionChangeSource] = useState<
{ source: 'enhance'; mode: EnhancementMode } | 'edit' | null
@@ -454,41 +451,13 @@ export function EditFeatureDialog({
<div className="grid gap-3 grid-cols-2">
<div className="space-y-1.5">
<Label
className={cn(
'text-xs text-muted-foreground',
!modelSupportsPlanningMode && 'opacity-50'
)}
>
Planning
</Label>
{modelSupportsPlanningMode ? (
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="edit-feature-planning"
compact
/>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="edit-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Label className="text-xs text-muted-foreground">Planning</Label>
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="edit-feature-planning"
compact
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Options</Label>
@@ -514,20 +483,14 @@ export function EditFeatureDialog({
id="edit-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
}
disabled={planningMode === 'skip' || planningMode === 'lite'}
data-testid="edit-feature-require-approval-checkbox"
/>
<Label
htmlFor="edit-feature-require-approval"
className={cn(
'text-xs font-normal',
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
planningMode === 'skip' || planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}

View File

@@ -24,7 +24,6 @@ import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { isCursorModel, type PhaseModelEntry } from '@automaker/types';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface MassEditDialogProps {
open: boolean;
@@ -236,8 +235,6 @@ export function MassEditDialog({
const hasAnyApply = Object.values(applyState).some(Boolean);
const isCurrentModelCursor = isCursorModel(model);
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
// All models support planning mode via marker-based instructions in prompts
const modelSupportsPlanningMode = true;
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
@@ -277,64 +274,30 @@ export function MassEditDialog({
<div className="border-t border-border" />
{/* Planning Mode */}
{modelSupportsPlanningMode ? (
<FieldWrapper
label="Planning Mode"
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
willApply={applyState.planningMode || applyState.requirePlanApproval}
onApplyChange={(apply) =>
setApplyState((prev) => ({
...prev,
planningMode: apply,
requirePlanApproval: apply,
}))
}
>
<PlanningModeSelect
mode={planningMode}
onModeChange={(newMode) => {
setPlanningMode(newMode);
// Auto-suggest approval based on mode, but user can override
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
}}
requireApproval={requirePlanApproval}
onRequireApprovalChange={setRequirePlanApproval}
testIdPrefix="mass-edit-planning"
/>
</FieldWrapper>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
)}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Checkbox checked={false} disabled className="opacity-50" />
<Label className="text-sm font-medium text-muted-foreground">
Planning Mode
</Label>
</div>
</div>
<div className="opacity-50 pointer-events-none">
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="mass-edit-planning"
disabled
/>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<FieldWrapper
label="Planning Mode"
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
willApply={applyState.planningMode || applyState.requirePlanApproval}
onApplyChange={(apply) =>
setApplyState((prev) => ({
...prev,
planningMode: apply,
requirePlanApproval: apply,
}))
}
>
<PlanningModeSelect
mode={planningMode}
onModeChange={(newMode) => {
setPlanningMode(newMode);
// Auto-suggest approval based on mode, but user can override
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
}}
requireApproval={requirePlanApproval}
onRequireApprovalChange={setRequirePlanApproval}
testIdPrefix="mass-edit-planning"
/>
</FieldWrapper>
{/* Priority */}
<FieldWrapper

View File

@@ -3,6 +3,7 @@
*/
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
import type { ParsedTask } from '@automaker/types';
export interface ImageAttachment {
id?: string; // Optional - may not be present in messages loaded from server
@@ -339,14 +340,8 @@ export type AutoModeEvent =
featureId: string;
projectPath?: string;
taskId: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
tasks: Array<{
id: string;
description: string;
filePath?: string;
phase?: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
}>;
status: ParsedTask['status'];
tasks: ParsedTask[];
}
| {
type: 'auto_mode_summary';