mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
Compare commits
5 Commits
88864ad6bc
...
63cae19aec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63cae19aec | ||
|
|
c9e721bda7 | ||
|
|
d4b7a0c57d | ||
|
|
0b6e84ec6e | ||
|
|
e9c2afcc02 |
25
README.md
25
README.md
@@ -288,6 +288,31 @@ services:
|
||||
|
||||
**Note:** The Claude CLI config must be writable (do not use `:ro` flag) as the CLI writes debug files.
|
||||
|
||||
> **⚠️ Important: Linux/WSL Users**
|
||||
>
|
||||
> The container runs as UID 1001 by default. If your host user has a different UID (common on Linux/WSL where the first user is UID 1000), you must create a `.env` file to match your host user:
|
||||
>
|
||||
> ```bash
|
||||
> # Check your UID/GID
|
||||
> id -u # outputs your UID (e.g., 1000)
|
||||
> id -g # outputs your GID (e.g., 1000)
|
||||
> ```
|
||||
>
|
||||
> Create a `.env` file in the automaker directory:
|
||||
>
|
||||
> ```
|
||||
> UID=1000
|
||||
> GID=1000
|
||||
> ```
|
||||
>
|
||||
> Then rebuild the images:
|
||||
>
|
||||
> ```bash
|
||||
> docker compose build
|
||||
> ```
|
||||
>
|
||||
> Without this, files written by the container will be inaccessible to your host user.
|
||||
|
||||
##### GitHub CLI Authentication (For Git Push/PR Operations)
|
||||
|
||||
To enable git push and GitHub CLI operations inside the container:
|
||||
|
||||
@@ -14,6 +14,7 @@ import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { findCliInWsl, isWslAvailable } from '@automaker/platform';
|
||||
import {
|
||||
CliProvider,
|
||||
type CliSpawnConfig,
|
||||
@@ -286,15 +287,113 @@ export class CursorProvider extends CliProvider {
|
||||
|
||||
getSpawnConfig(): CliSpawnConfig {
|
||||
return {
|
||||
windowsStrategy: 'wsl', // cursor-agent requires WSL on Windows
|
||||
windowsStrategy: 'direct',
|
||||
commonPaths: {
|
||||
linux: [
|
||||
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
|
||||
'/usr/local/bin/cursor-agent',
|
||||
],
|
||||
darwin: [path.join(os.homedir(), '.local/bin/cursor-agent'), '/usr/local/bin/cursor-agent'],
|
||||
// Windows paths are not used - we check for WSL installation instead
|
||||
win32: [],
|
||||
win32: [
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'Cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor-agent.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'Cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor-agent.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'Cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'Cursor',
|
||||
'cursor.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor-agent.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor-agent.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'cursor',
|
||||
'resources',
|
||||
'app',
|
||||
'bin',
|
||||
'cursor.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'Programs',
|
||||
'cursor',
|
||||
'cursor.exe'
|
||||
),
|
||||
path.join(
|
||||
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
||||
'npm',
|
||||
'cursor-agent.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
||||
'npm',
|
||||
'cursor.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
||||
'.npm-global',
|
||||
'bin',
|
||||
'cursor-agent.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
||||
'.npm-global',
|
||||
'bin',
|
||||
'cursor.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'pnpm',
|
||||
'cursor-agent.cmd'
|
||||
),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||
'pnpm',
|
||||
'cursor.cmd'
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -487,6 +586,92 @@ export class CursorProvider extends CliProvider {
|
||||
* 2. Cursor IDE with 'cursor agent' subcommand support
|
||||
*/
|
||||
protected detectCli(): CliDetectionResult {
|
||||
if (process.platform === 'win32') {
|
||||
const findInPath = (command: string): string | null => {
|
||||
try {
|
||||
const result = execSync(`where ${command}`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
.trim()
|
||||
.split(/\r?\n/)[0];
|
||||
|
||||
if (result && fs.existsSync(result)) {
|
||||
return result;
|
||||
}
|
||||
} catch {
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isCursorAgentBinary = (cliPath: string) =>
|
||||
cliPath.toLowerCase().includes('cursor-agent');
|
||||
|
||||
const supportsCursorAgentSubcommand = (cliPath: string) => {
|
||||
try {
|
||||
execSync(`"${cliPath}" agent --version`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: 'pipe',
|
||||
windowsHide: true,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const pathResult = findInPath('cursor-agent') || findInPath('cursor');
|
||||
if (pathResult) {
|
||||
if (isCursorAgentBinary(pathResult) || supportsCursorAgentSubcommand(pathResult)) {
|
||||
return {
|
||||
cliPath: pathResult,
|
||||
useWsl: false,
|
||||
strategy: pathResult.toLowerCase().endsWith('.cmd') ? 'cmd' : 'direct',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const config = this.getSpawnConfig();
|
||||
for (const candidate of config.commonPaths.win32 || []) {
|
||||
const resolved = candidate;
|
||||
if (!fs.existsSync(resolved)) {
|
||||
continue;
|
||||
}
|
||||
if (isCursorAgentBinary(resolved) || supportsCursorAgentSubcommand(resolved)) {
|
||||
return {
|
||||
cliPath: resolved,
|
||||
useWsl: false,
|
||||
strategy: resolved.toLowerCase().endsWith('.cmd') ? 'cmd' : 'direct',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const wslLogger = (msg: string) => logger.debug(msg);
|
||||
if (isWslAvailable({ logger: wslLogger })) {
|
||||
const wslResult = findCliInWsl('cursor-agent', { logger: wslLogger });
|
||||
if (wslResult) {
|
||||
logger.debug(
|
||||
`Using cursor-agent via WSL (${wslResult.distribution || 'default'}): ${wslResult.wslPath}`
|
||||
);
|
||||
return {
|
||||
cliPath: 'wsl.exe',
|
||||
useWsl: true,
|
||||
wslCliPath: wslResult.wslPath,
|
||||
wslDistribution: wslResult.distribution,
|
||||
strategy: 'wsl',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('cursor-agent not found on Windows');
|
||||
return { cliPath: null, useWsl: false, strategy: 'direct' };
|
||||
}
|
||||
|
||||
// First try standard detection (PATH, common paths, WSL)
|
||||
const result = super.detectCli();
|
||||
if (result.cliPath) {
|
||||
@@ -495,7 +680,7 @@ export class CursorProvider extends CliProvider {
|
||||
|
||||
// Cursor-specific: Check versions directory for any installed version
|
||||
// This handles cases where cursor-agent is installed but not in PATH
|
||||
if (process.platform !== 'win32' && fs.existsSync(CursorProvider.VERSIONS_DIR)) {
|
||||
if (fs.existsSync(CursorProvider.VERSIONS_DIR)) {
|
||||
try {
|
||||
const versions = fs
|
||||
.readdirSync(CursorProvider.VERSIONS_DIR)
|
||||
@@ -521,33 +706,31 @@ export class CursorProvider extends CliProvider {
|
||||
|
||||
// If cursor-agent not found, try to find 'cursor' IDE and use 'cursor agent' subcommand
|
||||
// The Cursor IDE includes the agent as a subcommand: cursor agent
|
||||
if (process.platform !== 'win32') {
|
||||
const cursorPaths = [
|
||||
'/usr/bin/cursor',
|
||||
'/usr/local/bin/cursor',
|
||||
path.join(os.homedir(), '.local/bin/cursor'),
|
||||
'/opt/cursor/cursor',
|
||||
];
|
||||
const cursorPaths = [
|
||||
'/usr/bin/cursor',
|
||||
'/usr/local/bin/cursor',
|
||||
path.join(os.homedir(), '.local/bin/cursor'),
|
||||
'/opt/cursor/cursor',
|
||||
];
|
||||
|
||||
for (const cursorPath of cursorPaths) {
|
||||
if (fs.existsSync(cursorPath)) {
|
||||
// Verify cursor agent subcommand works
|
||||
try {
|
||||
execSync(`"${cursorPath}" agent --version`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
logger.debug(`Using cursor agent via Cursor IDE: ${cursorPath}`);
|
||||
// Return cursor path but we'll use 'cursor agent' subcommand
|
||||
return {
|
||||
cliPath: cursorPath,
|
||||
useWsl: false,
|
||||
strategy: 'native',
|
||||
};
|
||||
} catch {
|
||||
// cursor agent subcommand doesn't work, try next path
|
||||
}
|
||||
for (const cursorPath of cursorPaths) {
|
||||
if (fs.existsSync(cursorPath)) {
|
||||
// Verify cursor agent subcommand works
|
||||
try {
|
||||
execSync(`"${cursorPath}" agent --version`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
logger.debug(`Using cursor agent via Cursor IDE: ${cursorPath}`);
|
||||
// Return cursor path but we'll use 'cursor agent' subcommand
|
||||
return {
|
||||
cliPath: cursorPath,
|
||||
useWsl: false,
|
||||
strategy: 'native',
|
||||
};
|
||||
} catch {
|
||||
// cursor agent subcommand doesn't work, try next path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,23 @@ import type { Request, Response } from 'express';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
import { CLAUDE_MODEL_MAP, type ThinkingLevel } from '@automaker/types';
|
||||
import { getAppSpecPath } from '@automaker/platform';
|
||||
import { simpleQuery } from '../../../providers/simple-query-service.js';
|
||||
import type { SettingsService } from '../../../services/settings-service.js';
|
||||
import { getPromptCustomization, getProviderByModelId } from '../../../lib/settings-helpers.js';
|
||||
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import {
|
||||
buildUserPrompt,
|
||||
isValidEnhancementMode,
|
||||
type EnhancementMode,
|
||||
} from '../../../lib/enhancement-prompts.js';
|
||||
import {
|
||||
extractTechnologyStack,
|
||||
extractXmlElements,
|
||||
extractXmlSection,
|
||||
unescapeXml,
|
||||
} from '../../../lib/xml-extractor.js';
|
||||
|
||||
const logger = createLogger('EnhancePrompt');
|
||||
|
||||
@@ -53,6 +62,66 @@ interface EnhanceErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
async function buildProjectContext(projectPath: string): Promise<string | null> {
|
||||
const contextBlocks: string[] = [];
|
||||
|
||||
try {
|
||||
const appSpecPath = getAppSpecPath(projectPath);
|
||||
const specContent = (await secureFs.readFile(appSpecPath, 'utf-8')) as string;
|
||||
|
||||
const projectName = extractXmlSection(specContent, 'project_name');
|
||||
const overview = extractXmlSection(specContent, 'overview');
|
||||
const techStack = extractTechnologyStack(specContent);
|
||||
const coreSection = extractXmlSection(specContent, 'core_capabilities');
|
||||
const coreCapabilities = coreSection ? extractXmlElements(coreSection, 'capability') : [];
|
||||
|
||||
const summaryLines: string[] = [];
|
||||
if (projectName) {
|
||||
summaryLines.push(`Name: ${unescapeXml(projectName.trim())}`);
|
||||
}
|
||||
if (overview) {
|
||||
summaryLines.push(`Overview: ${unescapeXml(overview.trim())}`);
|
||||
}
|
||||
if (techStack.length > 0) {
|
||||
summaryLines.push(`Tech Stack: ${techStack.join(', ')}`);
|
||||
}
|
||||
if (coreCapabilities.length > 0) {
|
||||
summaryLines.push(`Core Capabilities: ${coreCapabilities.slice(0, 10).join(', ')}`);
|
||||
}
|
||||
|
||||
if (summaryLines.length > 0) {
|
||||
contextBlocks.push(`PROJECT CONTEXT:\n${summaryLines.map((line) => `- ${line}`).join('\n')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('No app_spec.txt context available for enhancement', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const featureLoader = new FeatureLoader();
|
||||
const features = await featureLoader.getAll(projectPath);
|
||||
const featureTitles = features
|
||||
.map((feature) => feature.title || feature.name || feature.id)
|
||||
.filter((title) => Boolean(title));
|
||||
|
||||
if (featureTitles.length > 0) {
|
||||
const listed = featureTitles.slice(0, 30).map((title) => `- ${title}`);
|
||||
contextBlocks.push(
|
||||
`EXISTING FEATURES (avoid duplicates):\n${listed.join('\n')}${
|
||||
featureTitles.length > 30 ? '\n- ...' : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to load existing features for enhancement context', error);
|
||||
}
|
||||
|
||||
if (contextBlocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return contextBlocks.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the enhance request handler
|
||||
*
|
||||
@@ -122,6 +191,10 @@ export function createEnhanceHandler(
|
||||
|
||||
// Build the user prompt with few-shot examples
|
||||
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
|
||||
const projectContext = projectPath ? await buildProjectContext(projectPath) : null;
|
||||
if (projectContext) {
|
||||
logger.debug('Including project context in enhancement prompt');
|
||||
}
|
||||
|
||||
// Check if the model is a provider model (like "GLM-4.5-Air")
|
||||
// If so, get the provider config and resolved Claude model
|
||||
@@ -156,7 +229,7 @@ export function createEnhanceHandler(
|
||||
// The system prompt is combined with user prompt since some providers
|
||||
// don't have a separate system prompt concept
|
||||
const result = await simpleQuery({
|
||||
prompt: `${systemPrompt}\n\n${userPrompt}`,
|
||||
prompt: [systemPrompt, projectContext, userPrompt].filter(Boolean).join('\n\n'),
|
||||
model: resolvedModel,
|
||||
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
|
||||
maxTurns: 1,
|
||||
|
||||
@@ -437,6 +437,63 @@ export function BoardView() {
|
||||
// Auto mode hook - pass current worktree to get worktree-specific state
|
||||
// Must be after selectedWorktree is defined
|
||||
const autoMode = useAutoMode(selectedWorktree);
|
||||
|
||||
const refreshBoardState = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const projectPath = currentProject.path;
|
||||
const beforeFeatures = (
|
||||
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
|
||||
)?.length;
|
||||
const beforeWorktrees = (
|
||||
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
|
||||
| { worktrees?: unknown[] }
|
||||
| undefined
|
||||
)?.worktrees?.length;
|
||||
const beforeRunningAgents = (
|
||||
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
|
||||
)?.count;
|
||||
const beforeAutoModeRunning = autoMode.isRunning;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
queryClient.refetchQueries({ queryKey: queryKeys.features.all(projectPath) }),
|
||||
queryClient.refetchQueries({ queryKey: queryKeys.runningAgents.all() }),
|
||||
queryClient.refetchQueries({ queryKey: queryKeys.worktrees.all(projectPath) }),
|
||||
autoMode.refreshStatus(),
|
||||
]);
|
||||
|
||||
const afterFeatures = (
|
||||
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
|
||||
)?.length;
|
||||
const afterWorktrees = (
|
||||
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
|
||||
| { worktrees?: unknown[] }
|
||||
| undefined
|
||||
)?.worktrees?.length;
|
||||
const afterRunningAgents = (
|
||||
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
|
||||
)?.count;
|
||||
const afterAutoModeRunning = autoMode.isRunning;
|
||||
|
||||
if (
|
||||
beforeFeatures !== afterFeatures ||
|
||||
beforeWorktrees !== afterWorktrees ||
|
||||
beforeRunningAgents !== afterRunningAgents ||
|
||||
beforeAutoModeRunning !== afterAutoModeRunning
|
||||
) {
|
||||
logger.info('[Board] Refresh detected state mismatch', {
|
||||
features: { before: beforeFeatures, after: afterFeatures },
|
||||
worktrees: { before: beforeWorktrees, after: afterWorktrees },
|
||||
runningAgents: { before: beforeRunningAgents, after: afterRunningAgents },
|
||||
autoModeRunning: { before: beforeAutoModeRunning, after: afterAutoModeRunning },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[Board] Failed to refresh board state:', error);
|
||||
toast.error('Failed to refresh board state');
|
||||
}
|
||||
}, [autoMode, currentProject, queryClient]);
|
||||
// Get runningTasks from the hook (scoped to current project/worktree)
|
||||
const runningAutoTasks = autoMode.runningTasks;
|
||||
// Get worktree-specific maxConcurrency from the hook
|
||||
@@ -1321,6 +1378,7 @@ export function BoardView() {
|
||||
isCreatingSpec={isCreatingSpec}
|
||||
creatingSpecProjectPath={creatingSpecProjectPath}
|
||||
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
||||
onRefreshBoard={refreshBoardState}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Wand2, GitBranch, ClipboardCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Wand2, GitBranch, ClipboardCheck, RefreshCw } from 'lucide-react';
|
||||
import { UsagePopover } from '@/components/usage-popover';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
@@ -35,6 +37,7 @@ interface BoardHeaderProps {
|
||||
creatingSpecProjectPath?: string;
|
||||
// Board controls props
|
||||
onShowBoardBackground: () => void;
|
||||
onRefreshBoard: () => Promise<void>;
|
||||
// View toggle props
|
||||
viewMode: ViewMode;
|
||||
onViewModeChange: (mode: ViewMode) => void;
|
||||
@@ -60,6 +63,7 @@ export function BoardHeader({
|
||||
isCreatingSpec,
|
||||
creatingSpecProjectPath,
|
||||
onShowBoardBackground,
|
||||
onRefreshBoard,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
}: BoardHeaderProps) {
|
||||
@@ -110,9 +114,20 @@ export function BoardHeader({
|
||||
|
||||
// State for mobile actions panel
|
||||
const [showActionsPanel, setShowActionsPanel] = useState(false);
|
||||
const [isRefreshingBoard, setIsRefreshingBoard] = useState(false);
|
||||
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const handleRefreshBoard = useCallback(async () => {
|
||||
if (isRefreshingBoard) return;
|
||||
setIsRefreshingBoard(true);
|
||||
try {
|
||||
await onRefreshBoard();
|
||||
} finally {
|
||||
setIsRefreshingBoard(false);
|
||||
}
|
||||
}, [isRefreshingBoard, onRefreshBoard]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-5 p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -127,6 +142,22 @@ export function BoardHeader({
|
||||
<BoardControls isMounted={isMounted} onShowBoardBackground={onShowBoardBackground} />
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
{isMounted && !isTablet && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon-sm"
|
||||
onClick={handleRefreshBoard}
|
||||
disabled={isRefreshingBoard}
|
||||
aria-label="Refresh board state from server"
|
||||
>
|
||||
<RefreshCw className={isRefreshingBoard ? 'w-4 h-4 animate-spin' : 'w-4 h-4'} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Refresh board state from server</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
|
||||
{isMounted && !isTablet && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
|
||||
|
||||
|
||||
@@ -241,9 +241,9 @@ export function CreatePRDialog({
|
||||
<GitPullRequest className="w-5 h-5" />
|
||||
Create Pull Request
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="break-words">
|
||||
Push changes and create a pull request from{' '}
|
||||
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>
|
||||
<code className="font-mono bg-muted px-1 rounded break-all">{worktree.branch}</code>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { STALE_TIMES } from '@/lib/query-client';
|
||||
import { getGlobalEventsRecent } from '@/hooks/use-event-recency';
|
||||
import { createSmartPollingInterval, getGlobalEventsRecent } from '@/hooks/use-event-recency';
|
||||
import type { Feature } from '@/store/app-store';
|
||||
|
||||
const FEATURES_REFETCH_ON_FOCUS = false;
|
||||
const FEATURES_REFETCH_ON_RECONNECT = false;
|
||||
const FEATURES_POLLING_INTERVAL = 30000;
|
||||
/** Default polling interval for agent output when WebSocket is inactive */
|
||||
const AGENT_OUTPUT_POLLING_INTERVAL = 5000;
|
||||
|
||||
@@ -43,6 +44,7 @@ export function useFeatures(projectPath: string | undefined) {
|
||||
},
|
||||
enabled: !!projectPath,
|
||||
staleTime: STALE_TIMES.FEATURES,
|
||||
refetchInterval: createSmartPollingInterval(FEATURES_POLLING_INTERVAL),
|
||||
refetchOnWindowFocus: FEATURES_REFETCH_ON_FOCUS,
|
||||
refetchOnReconnect: FEATURES_REFETCH_ON_RECONNECT,
|
||||
});
|
||||
|
||||
@@ -9,9 +9,11 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { getElectronAPI, type RunningAgent } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { STALE_TIMES } from '@/lib/query-client';
|
||||
import { createSmartPollingInterval } from '@/hooks/use-event-recency';
|
||||
|
||||
const RUNNING_AGENTS_REFETCH_ON_FOCUS = false;
|
||||
const RUNNING_AGENTS_REFETCH_ON_RECONNECT = false;
|
||||
const RUNNING_AGENTS_POLLING_INTERVAL = 30000;
|
||||
|
||||
interface RunningAgentsResult {
|
||||
agents: RunningAgent[];
|
||||
@@ -47,8 +49,7 @@ export function useRunningAgents() {
|
||||
};
|
||||
},
|
||||
staleTime: STALE_TIMES.RUNNING_AGENTS,
|
||||
// Note: Don't use refetchInterval here - rely on WebSocket invalidation
|
||||
// for real-time updates instead of polling
|
||||
refetchInterval: createSmartPollingInterval(RUNNING_AGENTS_POLLING_INTERVAL),
|
||||
refetchOnWindowFocus: RUNNING_AGENTS_REFETCH_ON_FOCUS,
|
||||
refetchOnReconnect: RUNNING_AGENTS_REFETCH_ON_RECONNECT,
|
||||
});
|
||||
|
||||
@@ -8,9 +8,11 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { STALE_TIMES } from '@/lib/query-client';
|
||||
import { createSmartPollingInterval } from '@/hooks/use-event-recency';
|
||||
|
||||
const WORKTREE_REFETCH_ON_FOCUS = false;
|
||||
const WORKTREE_REFETCH_ON_RECONNECT = false;
|
||||
const WORKTREES_POLLING_INTERVAL = 30000;
|
||||
|
||||
interface WorktreeInfo {
|
||||
path: string;
|
||||
@@ -65,6 +67,7 @@ export function useWorktrees(projectPath: string | undefined, includeDetails = t
|
||||
},
|
||||
enabled: !!projectPath,
|
||||
staleTime: STALE_TIMES.WORKTREES,
|
||||
refetchInterval: createSmartPollingInterval(WORKTREES_POLLING_INTERVAL),
|
||||
refetchOnWindowFocus: WORKTREE_REFETCH_ON_FOCUS,
|
||||
refetchOnReconnect: WORKTREE_REFETCH_ON_RECONNECT,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,12 @@ import { useAppStore } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import type { AutoModeEvent } from '@/types/electron';
|
||||
import type { WorktreeInfo } from '@/components/views/board-view/worktree-panel/types';
|
||||
import { getGlobalEventsRecent } from '@/hooks/use-event-recency';
|
||||
|
||||
const logger = createLogger('AutoMode');
|
||||
|
||||
const AUTO_MODE_SESSION_KEY = 'automaker:autoModeRunningByWorktreeKey';
|
||||
const AUTO_MODE_POLLING_INTERVAL = 30000;
|
||||
|
||||
/**
|
||||
* Generate a worktree key for session storage
|
||||
@@ -140,42 +142,54 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
// Check if we can start a new task based on concurrency limit
|
||||
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.status) return;
|
||||
|
||||
const result = await api.autoMode.status(currentProject.path, branchName);
|
||||
if (result.success && result.isAutoLoopRunning !== undefined) {
|
||||
const backendIsRunning = result.isAutoLoopRunning;
|
||||
|
||||
if (backendIsRunning !== isAutoModeRunning) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
logger.info(
|
||||
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
|
||||
);
|
||||
setAutoModeRunning(
|
||||
currentProject.id,
|
||||
branchName,
|
||||
backendIsRunning,
|
||||
result.maxConcurrency,
|
||||
result.runningFeatures
|
||||
);
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error syncing auto mode state with backend:', error);
|
||||
}
|
||||
}, [branchName, currentProject, isAutoModeRunning, setAutoModeRunning]);
|
||||
|
||||
// On mount, query backend for current auto loop status and sync UI state.
|
||||
// This handles cases where the backend is still running after a page refresh.
|
||||
useEffect(() => {
|
||||
void refreshStatus();
|
||||
}, [refreshStatus]);
|
||||
|
||||
// Periodic polling fallback when WebSocket events are stale.
|
||||
useEffect(() => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const syncWithBackend = async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.status) return;
|
||||
const interval = setInterval(() => {
|
||||
if (getGlobalEventsRecent()) return;
|
||||
void refreshStatus();
|
||||
}, AUTO_MODE_POLLING_INTERVAL);
|
||||
|
||||
const result = await api.autoMode.status(currentProject.path, branchName);
|
||||
if (result.success && result.isAutoLoopRunning !== undefined) {
|
||||
const backendIsRunning = result.isAutoLoopRunning;
|
||||
|
||||
if (backendIsRunning !== isAutoModeRunning) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
logger.info(
|
||||
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
|
||||
);
|
||||
setAutoModeRunning(
|
||||
currentProject.id,
|
||||
branchName,
|
||||
backendIsRunning,
|
||||
result.maxConcurrency,
|
||||
result.runningFeatures
|
||||
);
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error syncing auto mode state with backend:', error);
|
||||
}
|
||||
};
|
||||
|
||||
syncWithBackend();
|
||||
}, [currentProject, branchName, setAutoModeRunning]);
|
||||
return () => clearInterval(interval);
|
||||
}, [currentProject, refreshStatus]);
|
||||
|
||||
// Handle auto mode events - listen globally for all projects/worktrees
|
||||
useEffect(() => {
|
||||
@@ -672,5 +686,6 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
start,
|
||||
stop,
|
||||
stopFeature,
|
||||
refreshStatus,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user