Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
e79252be5c 📝 Add docstrings to fix/pipeline-resume-edge-cases
Docstrings generation was requested by @casiusss.

* https://github.com/AutoMaker-Org/automaker/pull/344#issuecomment-3705368737

The following files were modified:

* `apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx`
* `apps/ui/src/components/views/board-view/hooks/use-board-effects.ts`
2026-01-02 13:54:30 +00:00
11 changed files with 163 additions and 145 deletions

1
.claude/.gitignore vendored
View File

@@ -1 +0,0 @@
hans/

View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -13,8 +13,7 @@ import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store'; import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
import { getHttpApiClient } from '@/lib/http-api-client'; import { getHttpApiClient, getServerUrlSync } from '@/lib/http-api-client';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import { useBoardBackgroundSettings } from '@/hooks/use-board-background-settings'; import { useBoardBackgroundSettings } from '@/hooks/use-board-background-settings';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
@@ -63,13 +62,12 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa
// Update preview image when background settings change // Update preview image when background settings change
useEffect(() => { useEffect(() => {
if (currentProject && backgroundSettings.imagePath) { if (currentProject && backgroundSettings.imagePath) {
const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync();
// Add cache-busting query parameter to force browser to reload image // Add cache-busting query parameter to force browser to reload image
const cacheBuster = imageVersion ?? Date.now().toString(); const cacheBuster = imageVersion ? `&v=${imageVersion}` : `&v=${Date.now()}`;
const imagePath = getAuthenticatedImageUrl( const imagePath = `${serverUrl}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath, backgroundSettings.imagePath
currentProject.path, )}&projectPath=${encodeURIComponent(currentProject.path)}${cacheBuster}`;
cacheBuster
);
setPreviewImage(imagePath); setPreviewImage(imagePath);
} else { } else {
setPreviewImage(null); setPreviewImage(null);

View File

@@ -3,7 +3,7 @@ import { cn } from '@/lib/utils';
import { ImageIcon, X, Loader2, FileText } from 'lucide-react'; import { ImageIcon, X, Loader2, FileText } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { getServerUrlSync } from '@/lib/http-api-client';
import { useAppStore, type FeatureImagePath, type FeatureTextFilePath } from '@/store/app-store'; import { useAppStore, type FeatureImagePath, type FeatureTextFilePath } from '@/store/app-store';
import { import {
sanitizeFilename, sanitizeFilename,
@@ -94,8 +94,9 @@ export function DescriptionImageDropZone({
// Construct server URL for loading saved images // Construct server URL for loading saved images
const getImageServerUrl = useCallback( const getImageServerUrl = useCallback(
(imagePath: string): string => { (imagePath: string): string => {
const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync();
const projectPath = currentProject?.path || ''; const projectPath = currentProject?.path || '';
return getAuthenticatedImageUrl(imagePath, projectPath); return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`;
}, },
[currentProject?.path] [currentProject?.path]
); );

View File

@@ -30,6 +30,29 @@ interface CardActionsProps {
onApprovePlan?: () => void; onApprovePlan?: () => void;
} }
/**
* Render contextual action buttons for a feature row based on the feature's status and whether it is the current automated task.
*
* Renders an appropriate set of buttons (Approve Plan, Logs, Force Stop, Verify, Resume, Complete, Edit, View Plan, Make, Refine, etc.) depending on
* feature properties (status, planSpec, skipTests, prUrl, id), the isCurrentAutoTask flag, and which callback props are provided.
*
* @param feature - The feature object whose status and metadata determine which actions are shown.
* @param isCurrentAutoTask - When true, renders actions relevant to the currently running automated task.
* @param hasContext - If true, indicates the feature has surrounding context (affects layout/availability in some states).
* @param shortcutKey - Optional keyboard shortcut label shown next to the Logs button when present.
* @param onEdit - Invoked when the Edit button is clicked.
* @param onViewOutput - Invoked when a Logs/View Output button is clicked.
* @param onVerify - Invoked when the Verify button (verification pathway) is clicked.
* @param onResume - Invoked when the Resume button is clicked.
* @param onForceStop - Invoked when the Force Stop button is clicked.
* @param onManualVerify - Invoked when a manual verification button is clicked.
* @param onFollowUp - Invoked when the Refine/Follow-up button is clicked.
* @param onImplement - Invoked when the Make/Implement button is clicked.
* @param onComplete - Invoked when the Complete button is clicked.
* @param onViewPlan - Invoked when the View Plan button is clicked.
* @param onApprovePlan - Invoked when the Approve Plan button is clicked.
* @returns The JSX element containing the action buttons for the feature row.
*/
export function CardActions({ export function CardActions({
feature, feature,
isCurrentAutoTask, isCurrentAutoTask,
@@ -109,73 +132,90 @@ export function CardActions({
)} )}
</> </>
)} )}
{!isCurrentAutoTask && feature.status === 'in_progress' && ( {!isCurrentAutoTask &&
<> (feature.status === 'in_progress' ||
{/* Approve Plan button - shows when plan is generated and waiting for approval */} (typeof feature.status === 'string' && feature.status.startsWith('pipeline_'))) && (
{feature.planSpec?.status === 'generated' && onApprovePlan && ( <>
<Button {/* Approve Plan button - shows when plan is generated and waiting for approval */}
variant="default" {feature.planSpec?.status === 'generated' && onApprovePlan && (
size="sm" <Button
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse" variant="default"
onClick={(e) => { size="sm"
e.stopPropagation(); className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
onApprovePlan(); onClick={(e) => {
}} e.stopPropagation();
onPointerDown={(e) => e.stopPropagation()} onApprovePlan();
data-testid={`approve-plan-${feature.id}`} }}
> onPointerDown={(e) => e.stopPropagation()}
<FileText className="w-3 h-3 mr-1" /> data-testid={`approve-plan-${feature.id}`}
Approve Plan >
</Button> <FileText className="w-3 h-3 mr-1" />
)} Approve Plan
{feature.skipTests && onManualVerify ? ( </Button>
<Button )}
variant="default" {feature.skipTests && onManualVerify ? (
size="sm" <Button
className="flex-1 h-7 text-[11px]" variant="default"
onClick={(e) => { size="sm"
e.stopPropagation(); className="flex-1 h-7 text-[11px]"
onManualVerify(); onClick={(e) => {
}} e.stopPropagation();
onPointerDown={(e) => e.stopPropagation()} onManualVerify();
data-testid={`manual-verify-${feature.id}`} }}
> onPointerDown={(e) => e.stopPropagation()}
<CheckCircle2 className="w-3 h-3 mr-1" /> data-testid={`manual-verify-${feature.id}`}
Verify >
</Button> <CheckCircle2 className="w-3 h-3 mr-1" />
) : onResume ? ( Verify
<Button </Button>
variant="default" ) : onResume ? (
size="sm" <Button
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90" variant="default"
onClick={(e) => { size="sm"
e.stopPropagation(); className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
onResume(); onClick={(e) => {
}} e.stopPropagation();
onPointerDown={(e) => e.stopPropagation()} onResume();
data-testid={`resume-feature-${feature.id}`} }}
> onPointerDown={(e) => e.stopPropagation()}
<RotateCcw className="w-3 h-3 mr-1" /> data-testid={`resume-feature-${feature.id}`}
Resume >
</Button> <RotateCcw className="w-3 h-3 mr-1" />
) : null} Resume
{onViewOutput && !feature.skipTests && ( </Button>
<Button ) : onVerify ? (
variant="secondary" <Button
size="sm" variant="default"
className="h-7 text-[11px] px-2" size="sm"
onClick={(e) => { className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
e.stopPropagation(); onClick={(e) => {
onViewOutput(); e.stopPropagation();
}} onVerify();
onPointerDown={(e) => e.stopPropagation()} }}
data-testid={`view-output-inprogress-${feature.id}`} onPointerDown={(e) => e.stopPropagation()}
> data-testid={`verify-feature-${feature.id}`}
<FileText className="w-3 h-3" /> >
</Button> <CheckCircle2 className="w-3 h-3 mr-1" />
)} Verify
</> </Button>
)} ) : null}
{onViewOutput && !feature.skipTests && (
<Button
variant="secondary"
size="sm"
className="h-7 text-[11px] px-2"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`view-output-inprogress-${feature.id}`}
>
<FileText className="w-3 h-3" />
</Button>
)}
</>
)}
{!isCurrentAutoTask && feature.status === 'verified' && ( {!isCurrentAutoTask && feature.status === 'verified' && (
<> <>
{/* Logs button */} {/* Logs button */}

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store'; import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { getServerUrlSync } from '@/lib/http-api-client';
interface UseBoardBackgroundProps { interface UseBoardBackgroundProps {
currentProject: { path: string; id: string } | null; currentProject: { path: string; id: string } | null;
@@ -22,14 +22,14 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps)
return {}; return {};
} }
const imageUrl = getAuthenticatedImageUrl(
backgroundSettings.imagePath,
currentProject.path,
backgroundSettings.imageVersion
);
return { return {
backgroundImage: `url(${imageUrl})`, backgroundImage: `url(${
import.meta.env.VITE_SERVER_URL || getServerUrlSync()
}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath
)}&projectPath=${encodeURIComponent(currentProject.path)}${
backgroundSettings.imageVersion ? `&v=${backgroundSettings.imageVersion}` : ''
})`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',

View File

@@ -16,6 +16,23 @@ interface UseBoardEffectsProps {
setFeaturesWithContext: (set: Set<string>) => void; setFeaturesWithContext: (set: Set<string>) => void;
} }
/**
* Registers and manages side effects for the board view (IPC/event listeners, global exposure, and context checks).
*
* Sets up event subscriptions to suggestions, spec regeneration, and auto-mode events; exposes the current project globally for modals; syncs running tasks from the backend; and maintains the set of feature IDs that have associated context files.
*
* @param currentProject - The active project object or `null`. Exposed globally for modal use and used when syncing backend state.
* @param specCreatingForProject - Project path currently undergoing spec regeneration, or `null`.
* @param setSpecCreatingForProject - Setter to clear or set the spec-regenerating project path.
* @param setSuggestionsCount - Setter for the persisted number of suggestion items.
* @param setFeatureSuggestions - Setter for the latest suggestion payload.
* @param setIsGeneratingSuggestions - Setter to mark whether suggestions are being generated.
* @param checkContextExists - Async function that returns whether a given feature ID has context files.
* @param features - Array of feature records to evaluate for potential context files.
* @param isLoading - Flag indicating whether features are still loading; context checks run only when loading is complete.
* @param featuresWithContext - Set of feature IDs currently known to have context files.
* @param setFeaturesWithContext - Setter that replaces the set of feature IDs that have context files.
*/
export function useBoardEffects({ export function useBoardEffects({
currentProject, currentProject,
specCreatingForProject, specCreatingForProject,
@@ -130,7 +147,10 @@ export function useBoardEffects({
const checkAllContexts = async () => { const checkAllContexts = async () => {
const featuresWithPotentialContext = features.filter( const featuresWithPotentialContext = features.filter(
(f) => (f) =>
f.status === 'in_progress' || f.status === 'waiting_approval' || f.status === 'verified' f.status === 'in_progress' ||
f.status === 'waiting_approval' ||
f.status === 'verified' ||
(typeof f.status === 'string' && f.status.startsWith('pipeline_'))
); );
const contextChecks = await Promise.all( const contextChecks = await Promise.all(
featuresWithPotentialContext.map(async (f) => ({ featuresWithPotentialContext.map(async (f) => ({

View File

@@ -153,37 +153,3 @@ export async function apiDeleteRaw(
): Promise<Response> { ): Promise<Response> {
return apiFetch(endpoint, 'DELETE', options); 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

@@ -5,7 +5,6 @@
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { Buffer } from 'buffer';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import {
@@ -119,10 +118,21 @@ test.describe('Add Context Image', () => {
test('should import an image file to context', async ({ page }) => { test('should import an image file to context', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath()); await setupProjectWithFixture(page, getFixturePath());
await authenticateForTests(page);
await page.goto('/'); await page.goto('/');
await waitForNetworkIdle(page); await waitForNetworkIdle(page);
// Check if we're on the login screen and authenticate if needed
const loginInput = page.locator('input[type="password"][placeholder*="API key"]');
const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
if (isLoginScreen) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInput.fill(apiKey);
await page.locator('button:has-text("Login")').click();
await page.waitForURL('**/', { timeout: 5000 });
await waitForNetworkIdle(page);
}
await navigateToContext(page); await navigateToContext(page);
// Wait for the file input to be attached to the DOM before setting files // Wait for the file input to be attached to the DOM before setting files

View File

@@ -516,7 +516,6 @@ async function main() {
console.log('═══════════════════════════════════════════════════════'); console.log('═══════════════════════════════════════════════════════');
console.log(' 1) Web Application (Browser)'); console.log(' 1) Web Application (Browser)');
console.log(' 2) Desktop Application (Electron)'); console.log(' 2) Desktop Application (Electron)');
console.log(' 3) Docker Container');
console.log('═══════════════════════════════════════════════════════'); console.log('═══════════════════════════════════════════════════════');
console.log(''); console.log('');
@@ -534,7 +533,7 @@ async function main() {
// Prompt for choice // Prompt for choice
while (true) { while (true) {
const choice = await prompt('Enter your choice (1, 2, or 3): '); const choice = await prompt('Enter your choice (1 or 2): ');
if (choice === '1') { if (choice === '1') {
console.log(''); console.log('');
@@ -635,23 +634,9 @@ async function main() {
electronProcess.on('close', resolve); electronProcess.on('close', resolve);
}); });
break;
} else if (choice === '3') {
console.log('');
log('Launching Docker Container...', 'blue');
console.log('');
// Run docker compose up --build via npm run dev:docker
const dockerProcess = runNpm(['run', 'dev:docker'], {
stdio: 'inherit',
});
await new Promise((resolve) => {
dockerProcess.on('close', resolve);
});
break; break;
} else { } else {
log('Invalid choice. Please enter 1, 2, or 3.', 'red'); log('Invalid choice. Please enter 1 or 2.', 'red');
} }
} }
} }