mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
various fixes
This commit is contained in:
106
.github/scripts/upload-to-r2.js
vendored
106
.github/scripts/upload-to-r2.js
vendored
@@ -1,10 +1,14 @@
|
|||||||
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
const {
|
||||||
const fs = require('fs');
|
S3Client,
|
||||||
const path = require('path');
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
} = require("@aws-sdk/client-s3");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: 'auto',
|
region: "auto",
|
||||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||||
@@ -18,15 +22,17 @@ const GITHUB_REPO = process.env.GITHUB_REPOSITORY;
|
|||||||
|
|
||||||
async function fetchExistingReleases() {
|
async function fetchExistingReleases() {
|
||||||
try {
|
try {
|
||||||
const response = await s3Client.send(new GetObjectCommand({
|
const response = await s3Client.send(
|
||||||
Bucket: BUCKET,
|
new GetObjectCommand({
|
||||||
Key: 'releases.json',
|
Bucket: BUCKET,
|
||||||
}));
|
Key: "releases.json",
|
||||||
|
})
|
||||||
|
);
|
||||||
const body = await response.Body.transformToString();
|
const body = await response.Body.transformToString();
|
||||||
return JSON.parse(body);
|
return JSON.parse(body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
||||||
console.log('No existing releases.json found, creating new one');
|
console.log("No existing releases.json found, creating new one");
|
||||||
return { latestVersion: null, releases: [] };
|
return { latestVersion: null, releases: [] };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -37,12 +43,14 @@ async function uploadFile(localPath, r2Key, contentType) {
|
|||||||
const fileBuffer = fs.readFileSync(localPath);
|
const fileBuffer = fs.readFileSync(localPath);
|
||||||
const stats = fs.statSync(localPath);
|
const stats = fs.statSync(localPath);
|
||||||
|
|
||||||
await s3Client.send(new PutObjectCommand({
|
await s3Client.send(
|
||||||
Bucket: BUCKET,
|
new PutObjectCommand({
|
||||||
Key: r2Key,
|
Bucket: BUCKET,
|
||||||
Body: fileBuffer,
|
Key: r2Key,
|
||||||
ContentType: contentType,
|
Body: fileBuffer,
|
||||||
}));
|
ContentType: contentType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Uploaded: ${r2Key} (${stats.size} bytes)`);
|
console.log(`Uploaded: ${r2Key} (${stats.size} bytes)`);
|
||||||
return stats.size;
|
return stats.size;
|
||||||
@@ -51,44 +59,44 @@ async function uploadFile(localPath, r2Key, contentType) {
|
|||||||
function findArtifacts(dir, pattern) {
|
function findArtifacts(dir, pattern) {
|
||||||
if (!fs.existsSync(dir)) return [];
|
if (!fs.existsSync(dir)) return [];
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
return files.filter(f => pattern.test(f)).map(f => path.join(dir, f));
|
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const artifactsDir = 'artifacts';
|
const artifactsDir = "artifacts";
|
||||||
|
|
||||||
// Find all artifacts
|
// Find all artifacts
|
||||||
const artifacts = {
|
const artifacts = {
|
||||||
windows: findArtifacts(
|
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
|
||||||
path.join(artifactsDir, 'windows-builds'),
|
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
|
||||||
/\.exe$/
|
|
||||||
),
|
|
||||||
macos: findArtifacts(
|
|
||||||
path.join(artifactsDir, 'macos-builds'),
|
|
||||||
/-x64\.dmg$/
|
|
||||||
),
|
|
||||||
macosArm: findArtifacts(
|
macosArm: findArtifacts(
|
||||||
path.join(artifactsDir, 'macos-builds'),
|
path.join(artifactsDir, "macos-builds"),
|
||||||
/-arm64\.dmg$/
|
/-arm64\.dmg$/
|
||||||
),
|
),
|
||||||
linux: findArtifacts(
|
linux: findArtifacts(
|
||||||
path.join(artifactsDir, 'linux-builds'),
|
path.join(artifactsDir, "linux-builds"),
|
||||||
/\.AppImage$/
|
/\.AppImage$/
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Found artifacts:');
|
console.log("Found artifacts:");
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
console.log(` ${platform}: ${files.length > 0 ? files.map(f => path.basename(f)).join(', ') : 'none'}`);
|
console.log(
|
||||||
|
` ${platform}: ${
|
||||||
|
files.length > 0
|
||||||
|
? files.map((f) => path.basename(f)).join(", ")
|
||||||
|
: "none"
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload each artifact to R2
|
// Upload each artifact to R2
|
||||||
const assets = {};
|
const assets = {};
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
windows: 'application/x-msdownload',
|
windows: "application/x-msdownload",
|
||||||
macos: 'application/x-apple-diskimage',
|
macos: "application/x-apple-diskimage",
|
||||||
macosArm: 'application/x-apple-diskimage',
|
macosArm: "application/x-apple-diskimage",
|
||||||
linux: 'application/x-executable',
|
linux: "application/x-executable",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
@@ -107,7 +115,7 @@ async function main() {
|
|||||||
url: `${PUBLIC_URL}/releases/${VERSION}/${filename}`,
|
url: `${PUBLIC_URL}/releases/${VERSION}/${filename}`,
|
||||||
filename,
|
filename,
|
||||||
size,
|
size,
|
||||||
arch: platform === 'macosArm' ? 'arm64' : 'x64',
|
arch: platform === "macosArm" ? "arm64" : "x64",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,27 +130,31 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove existing entry for this version if re-running
|
// Remove existing entry for this version if re-running
|
||||||
releasesData.releases = releasesData.releases.filter(r => r.version !== VERSION);
|
releasesData.releases = releasesData.releases.filter(
|
||||||
|
(r) => r.version !== VERSION
|
||||||
|
);
|
||||||
|
|
||||||
// Prepend new release
|
// Prepend new release
|
||||||
releasesData.releases.unshift(newRelease);
|
releasesData.releases.unshift(newRelease);
|
||||||
releasesData.latestVersion = VERSION;
|
releasesData.latestVersion = VERSION;
|
||||||
|
|
||||||
// Upload updated releases.json
|
// Upload updated releases.json
|
||||||
await s3Client.send(new PutObjectCommand({
|
await s3Client.send(
|
||||||
Bucket: BUCKET,
|
new PutObjectCommand({
|
||||||
Key: 'releases.json',
|
Bucket: BUCKET,
|
||||||
Body: JSON.stringify(releasesData, null, 2),
|
Key: "releases.json",
|
||||||
ContentType: 'application/json',
|
Body: JSON.stringify(releasesData, null, 2),
|
||||||
CacheControl: 'public, max-age=60',
|
ContentType: "application/json",
|
||||||
}));
|
CacheControl: "public, max-age=60",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Successfully updated releases.json');
|
console.log("Successfully updated releases.json");
|
||||||
console.log(`Latest version: ${VERSION}`);
|
console.log(`Latest version: ${VERSION}`);
|
||||||
console.log(`Total releases: ${releasesData.releases.length}`);
|
console.log(`Total releases: ${releasesData.releases.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
main().catch((err) => {
|
||||||
console.error('Failed to upload to R2:', err);
|
console.error("Failed to upload to R2:", err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to R2 and update releases.json
|
- name: Upload to R2 and update releases.json
|
||||||
env:
|
env:
|
||||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Bug,
|
Bug,
|
||||||
Activity,
|
Activity,
|
||||||
|
Recycle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -70,7 +71,7 @@ import {
|
|||||||
useKeyboardShortcutsConfig,
|
useKeyboardShortcutsConfig,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
|
import { getElectronAPI, Project, TrashedProject, RunningAgent } from "@/lib/electron";
|
||||||
import {
|
import {
|
||||||
initializeProject,
|
initializeProject,
|
||||||
hasAppSpec,
|
hasAppSpec,
|
||||||
@@ -80,6 +81,7 @@ import { toast } from "sonner";
|
|||||||
import { Sparkles, Loader2 } from "lucide-react";
|
import { Sparkles, Loader2 } from "lucide-react";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
|
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -212,6 +214,7 @@ export function Sidebar() {
|
|||||||
setProjectTheme,
|
setProjectTheme,
|
||||||
setTheme,
|
setTheme,
|
||||||
theme: globalTheme,
|
theme: globalTheme,
|
||||||
|
moveProjectToTrash,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Get customizable keyboard shortcuts
|
// Get customizable keyboard shortcuts
|
||||||
@@ -225,6 +228,12 @@ export function Sidebar() {
|
|||||||
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
||||||
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
|
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
|
||||||
|
|
||||||
|
// State for delete project confirmation dialog
|
||||||
|
const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false);
|
||||||
|
|
||||||
|
// State for running agents count
|
||||||
|
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
||||||
|
|
||||||
// State for new project setup dialog
|
// State for new project setup dialog
|
||||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||||
@@ -334,6 +343,64 @@ export function Sidebar() {
|
|||||||
};
|
};
|
||||||
}, [setCurrentView]);
|
}, [setCurrentView]);
|
||||||
|
|
||||||
|
// Fetch running agents count and update every 2 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRunningAgentsCount = async () => {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.runningAgents) {
|
||||||
|
const result = await api.runningAgents.getAll();
|
||||||
|
if (result.success && result.runningAgents) {
|
||||||
|
setRunningAgentsCount(result.runningAgents.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Sidebar] Error fetching running agents count:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
fetchRunningAgentsCount();
|
||||||
|
|
||||||
|
// Set up interval to refresh every 2 seconds
|
||||||
|
const interval = setInterval(fetchRunningAgentsCount, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Subscribe to auto-mode events to update running agents count in real-time
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.autoMode) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
||||||
|
// When a feature starts, completes, or errors, refresh the count
|
||||||
|
if (
|
||||||
|
event.type === "auto_mode_feature_complete" ||
|
||||||
|
event.type === "auto_mode_error" ||
|
||||||
|
event.type === "auto_mode_feature_started"
|
||||||
|
) {
|
||||||
|
const fetchRunningAgentsCount = async () => {
|
||||||
|
try {
|
||||||
|
if (api.runningAgents) {
|
||||||
|
const result = await api.runningAgents.getAll();
|
||||||
|
if (result.success && result.runningAgents) {
|
||||||
|
setRunningAgentsCount(result.runningAgents.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Sidebar] Error fetching running agents count:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchRunningAgentsCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle creating initial spec for new project
|
// Handle creating initial spec for new project
|
||||||
const handleCreateInitialSpec = useCallback(async () => {
|
const handleCreateInitialSpec = useCallback(async () => {
|
||||||
if (!setupProjectPath || !projectOverview.trim()) return;
|
if (!setupProjectPath || !projectOverview.trim()) return;
|
||||||
@@ -534,14 +601,14 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
"Clear all trashed projects from Automaker? This does not delete folders from disk."
|
"Clear all projects from recycle bin? This does not delete folders from disk."
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
setIsEmptyingTrash(true);
|
setIsEmptyingTrash(true);
|
||||||
try {
|
try {
|
||||||
emptyTrash();
|
emptyTrash();
|
||||||
toast.success("Trash cleared");
|
toast.success("Recycle bin cleared");
|
||||||
setShowTrashDialog(false);
|
setShowTrashDialog(false);
|
||||||
} finally {
|
} finally {
|
||||||
setIsEmptyingTrash(false);
|
setIsEmptyingTrash(false);
|
||||||
@@ -830,10 +897,10 @@ export function Sidebar() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setShowTrashDialog(true)}
|
onClick={() => setShowTrashDialog(true)}
|
||||||
className="group flex items-center justify-center px-3 h-[42px] rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border"
|
className="group flex items-center justify-center px-3 h-[42px] rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border"
|
||||||
title="Trash"
|
title="Recycle Bin"
|
||||||
data-testid="trash-button"
|
data-testid="trash-button"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 shrink-0" />
|
<Recycle className="size-4 shrink-0" />
|
||||||
{trashedProjects.length > 0 && (
|
{trashedProjects.length > 0 && (
|
||||||
<span className="absolute -top-[2px] -right-[2px] flex items-center justify-center w-5 h-5 text-[10px] font-medium rounded-full text-brand-500">
|
<span className="absolute -top-[2px] -right-[2px] flex items-center justify-center w-5 h-5 text-[10px] font-medium rounded-full text-brand-500">
|
||||||
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
|
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
|
||||||
@@ -1039,6 +1106,17 @@ export function Sidebar() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Move to Trash Section */}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => setShowDeleteProjectDialog(true)}
|
||||||
|
className="text-destructive focus:text-destructive focus:bg-destructive/10"
|
||||||
|
data-testid="move-project-to-trash"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
<span>Move to Trash</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
@@ -1242,7 +1320,7 @@ export function Sidebar() {
|
|||||||
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
|
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
|
||||||
<DialogContent className="bg-popover border-border max-w-2xl">
|
<DialogContent className="bg-popover border-border max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Trash</DialogTitle>
|
<DialogTitle>Recycle Bin</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Restore projects to the sidebar or delete their folders using your
|
Restore projects to the sidebar or delete their folders using your
|
||||||
system Trash.
|
system Trash.
|
||||||
@@ -1250,7 +1328,7 @@ export function Sidebar() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{trashedProjects.length === 0 ? (
|
{trashedProjects.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">Trash is empty.</p>
|
<p className="text-sm text-muted-foreground">Recycle bin is empty.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
|
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
|
||||||
{trashedProjects.map((project) => (
|
{trashedProjects.map((project) => (
|
||||||
@@ -1318,7 +1396,7 @@ export function Sidebar() {
|
|||||||
disabled={isEmptyingTrash}
|
disabled={isEmptyingTrash}
|
||||||
data-testid="empty-trash"
|
data-testid="empty-trash"
|
||||||
>
|
>
|
||||||
{isEmptyingTrash ? "Clearing..." : "Empty Trash"}
|
{isEmptyingTrash ? "Clearing..." : "Empty Recycle Bin"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -1421,6 +1499,14 @@ export function Sidebar() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete Project Confirmation Dialog */}
|
||||||
|
<DeleteProjectDialog
|
||||||
|
open={showDeleteProjectDialog}
|
||||||
|
onOpenChange={setShowDeleteProjectDialog}
|
||||||
|
project={currentProject}
|
||||||
|
onConfirm={moveProjectToTrash}
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
|
FolderOpen,
|
||||||
Rocket,
|
Rocket,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Check,
|
Check,
|
||||||
@@ -28,6 +29,7 @@ import { starterTemplates, type StarterTemplate } from "@/lib/templates";
|
|||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
projectName?: boolean;
|
projectName?: boolean;
|
||||||
@@ -69,6 +71,7 @@ export function NewProjectModal({
|
|||||||
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
||||||
const [customUrl, setCustomUrl] = useState("");
|
const [customUrl, setCustomUrl] = useState("");
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||||
|
const { openFileBrowser } = useFileBrowser();
|
||||||
|
|
||||||
// Fetch workspace directory when modal opens
|
// Fetch workspace directory when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -181,6 +184,20 @@ export function NewProjectModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBrowseDirectory = async () => {
|
||||||
|
const selectedPath = await openFileBrowser({
|
||||||
|
title: "Select Base Project Directory",
|
||||||
|
description: "Choose the parent directory where your project will be created",
|
||||||
|
});
|
||||||
|
if (selectedPath) {
|
||||||
|
setWorkspaceDir(selectedPath);
|
||||||
|
// Clear any workspace error when a valid directory is selected
|
||||||
|
if (errors.workspaceDir) {
|
||||||
|
setErrors((prev) => ({ ...prev, workspaceDir: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : "";
|
const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -226,16 +243,28 @@ export function NewProjectModal({
|
|||||||
"flex items-center gap-2 text-sm",
|
"flex items-center gap-2 text-sm",
|
||||||
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
||||||
)}>
|
)}>
|
||||||
<Folder className="w-4 h-4" />
|
<Folder className="w-4 h-4 shrink-0" />
|
||||||
<span>
|
<span className="flex-1 min-w-0">
|
||||||
{isLoadingWorkspace ? (
|
{isLoadingWorkspace ? (
|
||||||
"Loading workspace..."
|
"Loading workspace..."
|
||||||
) : workspaceDir ? (
|
) : workspaceDir ? (
|
||||||
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded">{projectPath || "..."}</code></>
|
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">{projectPath || "..."}</code></>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-red-500">No workspace configured - please configure WORKSPACE_DIR</span>
|
<span className="text-red-500">No workspace configured</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBrowseDirectory}
|
||||||
|
disabled={isLoadingWorkspace}
|
||||||
|
className="shrink-0 h-7 px-2 text-xs"
|
||||||
|
data-testid="browse-directory-button"
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -867,7 +867,8 @@ export function BoardView() {
|
|||||||
// features often have skipTests=true, and we want status-based handling first
|
// features often have skipTests=true, and we want status-based handling first
|
||||||
if (targetStatus === "verified") {
|
if (targetStatus === "verified") {
|
||||||
moveFeature(featureId, "verified");
|
moveFeature(featureId, "verified");
|
||||||
persistFeatureUpdate(featureId, { status: "verified" });
|
// Clear justFinished flag when manually verifying via drag
|
||||||
|
persistFeatureUpdate(featureId, { status: "verified", justFinished: false });
|
||||||
toast.success("Feature verified", {
|
toast.success("Feature verified", {
|
||||||
description: `Manually verified: ${draggedFeature.description.slice(
|
description: `Manually verified: ${draggedFeature.description.slice(
|
||||||
0,
|
0,
|
||||||
@@ -877,7 +878,8 @@ export function BoardView() {
|
|||||||
} else if (targetStatus === "backlog") {
|
} else if (targetStatus === "backlog") {
|
||||||
// Allow moving waiting_approval cards back to backlog
|
// Allow moving waiting_approval cards back to backlog
|
||||||
moveFeature(featureId, "backlog");
|
moveFeature(featureId, "backlog");
|
||||||
persistFeatureUpdate(featureId, { status: "backlog" });
|
// Clear justFinished flag when moving back to backlog
|
||||||
|
persistFeatureUpdate(featureId, { status: "backlog", justFinished: false });
|
||||||
toast.info("Feature moved to backlog", {
|
toast.info("Feature moved to backlog", {
|
||||||
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
||||||
0,
|
0,
|
||||||
@@ -1198,7 +1200,8 @@ export function BoardView() {
|
|||||||
description: feature.description,
|
description: feature.description,
|
||||||
});
|
});
|
||||||
moveFeature(feature.id, "verified");
|
moveFeature(feature.id, "verified");
|
||||||
persistFeatureUpdate(feature.id, { status: "verified" });
|
// Clear justFinished flag when manually verifying
|
||||||
|
persistFeatureUpdate(feature.id, { status: "verified", justFinished: false });
|
||||||
toast.success("Feature verified", {
|
toast.success("Feature verified", {
|
||||||
description: `Marked as verified: ${feature.description.slice(0, 50)}${
|
description: `Marked as verified: ${feature.description.slice(0, 50)}${
|
||||||
feature.description.length > 50 ? "..." : ""
|
feature.description.length > 50 ? "..." : ""
|
||||||
@@ -1264,9 +1267,11 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move feature back to in_progress before sending follow-up
|
// Move feature back to in_progress before sending follow-up
|
||||||
|
// Clear justFinished flag since user is now interacting with it
|
||||||
const updates = {
|
const updates = {
|
||||||
status: "in_progress" as const,
|
status: "in_progress" as const,
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
|
justFinished: false,
|
||||||
};
|
};
|
||||||
updateFeature(featureId, updates);
|
updateFeature(featureId, updates);
|
||||||
persistFeatureUpdate(featureId, updates);
|
persistFeatureUpdate(featureId, updates);
|
||||||
@@ -1525,6 +1530,14 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort waiting_approval column: justFinished features go to the top
|
||||||
|
map.waiting_approval.sort((a, b) => {
|
||||||
|
// Features with justFinished=true should appear first
|
||||||
|
if (a.justFinished && !b.justFinished) return -1;
|
||||||
|
if (!a.justFinished && b.justFinished) return 1;
|
||||||
|
return 0; // Keep original order for features with same justFinished status
|
||||||
|
});
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, [features, runningAutoTasks, searchQuery]);
|
}, [features, runningAutoTasks, searchQuery]);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
|
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||||
|
|
||||||
interface InterviewMessage {
|
interface InterviewMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,6 +66,7 @@ const INTERVIEW_QUESTIONS = [
|
|||||||
export function InterviewView() {
|
export function InterviewView() {
|
||||||
const { setCurrentView, addProject, setCurrentProject, setAppSpec } =
|
const { setCurrentView, addProject, setCurrentProject, setAppSpec } =
|
||||||
useAppStore();
|
useAppStore();
|
||||||
|
const { openFileBrowser } = useFileBrowser();
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [messages, setMessages] = useState<InterviewMessage[]>([]);
|
const [messages, setMessages] = useState<InterviewMessage[]>([]);
|
||||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||||
@@ -286,11 +288,13 @@ export function InterviewView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectDirectory = async () => {
|
const handleSelectDirectory = async () => {
|
||||||
const api = getElectronAPI();
|
const selectedPath = await openFileBrowser({
|
||||||
const result = await api.openDirectory();
|
title: "Select Base Directory",
|
||||||
|
description: "Choose the parent directory where your new project will be created",
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths[0]) {
|
if (selectedPath) {
|
||||||
setProjectPath(result.filePaths[0]);
|
setProjectPath(selectedPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Feature, useAppStore } from "@/store/app-store";
|
import { Feature, useAppStore, ThinkingLevel } from "@/store/app-store";
|
||||||
import {
|
import {
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Edit,
|
Edit,
|
||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
GitMerge,
|
GitMerge,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Brain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
@@ -72,6 +73,21 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats thinking level for compact display
|
||||||
|
*/
|
||||||
|
function formatThinkingLevel(level: ThinkingLevel | undefined): string {
|
||||||
|
if (!level || level === "none") return "";
|
||||||
|
const labels: Record<ThinkingLevel, string> = {
|
||||||
|
none: "",
|
||||||
|
low: "Low",
|
||||||
|
medium: "Med",
|
||||||
|
high: "High",
|
||||||
|
ultrathink: "Ultra",
|
||||||
|
};
|
||||||
|
return labels[level];
|
||||||
|
}
|
||||||
|
|
||||||
interface KanbanCardProps {
|
interface KanbanCardProps {
|
||||||
feature: Feature;
|
feature: Feature;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -280,6 +296,21 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
<span>Errored</span>
|
<span>Errored</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Just Finished indicator badge - shows when agent just completed work */}
|
||||||
|
{feature.justFinished && feature.status === "waiting_approval" && !feature.error && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
||||||
|
feature.skipTests ? "top-8 left-2" : "top-2 left-2",
|
||||||
|
"bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse"
|
||||||
|
)}
|
||||||
|
data-testid={`just-finished-badge-${feature.id}`}
|
||||||
|
title="Agent just finished working on this feature"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
<span>Done</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Branch badge - show when feature has a worktree */}
|
{/* Branch badge - show when feature has a worktree */}
|
||||||
{hasWorktree && !isCurrentAutoTask && (
|
{hasWorktree && !isCurrentAutoTask && (
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
@@ -289,8 +320,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
|
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
|
||||||
"bg-purple-500/20 border border-purple-500/50 text-purple-400",
|
"bg-purple-500/20 border border-purple-500/50 text-purple-400",
|
||||||
// Position below error badge if present, otherwise use normal position
|
// Position below other badges if present, otherwise use normal position
|
||||||
feature.error || feature.skipTests
|
feature.error || feature.skipTests || (feature.justFinished && feature.status === "waiting_approval")
|
||||||
? "top-8 left-2"
|
? "top-8 left-2"
|
||||||
: "top-2 left-2"
|
: "top-2 left-2"
|
||||||
)}
|
)}
|
||||||
@@ -310,14 +341,17 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
|
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
|
||||||
// Add extra top padding when badges are present to prevent text overlap
|
// Add extra top padding when badges are present to prevent text overlap
|
||||||
(feature.skipTests || feature.error) && "pt-10",
|
(feature.skipTests || feature.error || (feature.justFinished && feature.status === "waiting_approval")) && "pt-10",
|
||||||
// Add even more top padding when both badges and branch are shown
|
// Add even more top padding when both badges and branch are shown
|
||||||
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
|
hasWorktree && (feature.skipTests || feature.error || (feature.justFinished && feature.status === "waiting_approval")) && "pt-14"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCurrentAutoTask && (
|
{isCurrentAutoTask && (
|
||||||
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
||||||
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
|
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
|
||||||
|
<span className="text-xs text-running-indicator font-medium">
|
||||||
|
{formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||||
|
</span>
|
||||||
{feature.startedAt && (
|
{feature.startedAt && (
|
||||||
<CountUpTimer
|
<CountUpTimer
|
||||||
startedAt={feature.startedAt}
|
startedAt={feature.startedAt}
|
||||||
@@ -452,6 +486,28 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Model/Preset Info for Backlog Cards - Show in Detailed mode */}
|
||||||
|
{showAgentInfo && feature.status === "backlog" && (
|
||||||
|
<div className="mb-3 space-y-2 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||||
|
<div className="flex items-center gap-1 text-cyan-400">
|
||||||
|
<Cpu className="w-3 h-3" />
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{feature.thinkingLevel && feature.thinkingLevel !== "none" && (
|
||||||
|
<div className="flex items-center gap-1 text-purple-400">
|
||||||
|
<Brain className="w-3 h-3" />
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatThinkingLevel(feature.thinkingLevel)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||||
{/* Detailed mode: Show all agent info */}
|
{/* Detailed mode: Show all agent info */}
|
||||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||||
|
|||||||
@@ -181,7 +181,8 @@ export function WelcomeView() {
|
|||||||
if (!result.canceled && result.filePaths[0]) {
|
if (!result.canceled && result.filePaths[0]) {
|
||||||
const path = result.filePaths[0];
|
const path = result.filePaths[0];
|
||||||
// Extract folder name from path (works on both Windows and Mac/Linux)
|
// Extract folder name from path (works on both Windows and Mac/Linux)
|
||||||
const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
|
const name =
|
||||||
|
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
|
||||||
await initializeAndOpenProject(path, name);
|
await initializeAndOpenProject(path, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +194,8 @@ export function WelcomeView() {
|
|||||||
|
|
||||||
if (!result.canceled && result.filePaths[0]) {
|
if (!result.canceled && result.filePaths[0]) {
|
||||||
const path = result.filePaths[0];
|
const path = result.filePaths[0];
|
||||||
const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
|
const name =
|
||||||
|
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
|
||||||
await initializeAndOpenProject(path, name);
|
await initializeAndOpenProject(path, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +233,10 @@ export function WelcomeView() {
|
|||||||
/**
|
/**
|
||||||
* Create a blank project with just .automaker directory structure
|
* Create a blank project with just .automaker directory structure
|
||||||
*/
|
*/
|
||||||
const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
|
const handleCreateBlankProject = async (
|
||||||
|
projectName: string,
|
||||||
|
parentDir: string
|
||||||
|
) => {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -359,11 +364,15 @@ export function WelcomeView() {
|
|||||||
</overview>
|
</overview>
|
||||||
|
|
||||||
<technology_stack>
|
<technology_stack>
|
||||||
${template.techStack.map((tech) => `<technology>${tech}</technology>`).join("\n ")}
|
${template.techStack
|
||||||
|
.map((tech) => `<technology>${tech}</technology>`)
|
||||||
|
.join("\n ")}
|
||||||
</technology_stack>
|
</technology_stack>
|
||||||
|
|
||||||
<core_capabilities>
|
<core_capabilities>
|
||||||
${template.features.map((feature) => `<capability>${feature}</capability>`).join("\n ")}
|
${template.features
|
||||||
|
.map((feature) => `<capability>${feature}</capability>`)
|
||||||
|
.join("\n ")}
|
||||||
</core_capabilities>
|
</core_capabilities>
|
||||||
|
|
||||||
<implemented_features>
|
<implemented_features>
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
||||||
import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog";
|
import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog";
|
||||||
|
|
||||||
|
interface FileBrowserOptions {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FileBrowserContextValue {
|
interface FileBrowserContextValue {
|
||||||
openFileBrowser: () => Promise<string | null>;
|
openFileBrowser: (options?: FileBrowserOptions) => Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);
|
const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);
|
||||||
@@ -12,9 +17,11 @@ const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);
|
|||||||
export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [resolver, setResolver] = useState<((value: string | null) => void) | null>(null);
|
const [resolver, setResolver] = useState<((value: string | null) => void) | null>(null);
|
||||||
|
const [dialogOptions, setDialogOptions] = useState<FileBrowserOptions>({});
|
||||||
|
|
||||||
const openFileBrowser = useCallback((): Promise<string | null> => {
|
const openFileBrowser = useCallback((options?: FileBrowserOptions): Promise<string | null> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
setDialogOptions(options || {});
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setResolver(() => resolve);
|
setResolver(() => resolve);
|
||||||
});
|
});
|
||||||
@@ -26,6 +33,7 @@ export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
|||||||
setResolver(null);
|
setResolver(null);
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setDialogOptions({});
|
||||||
}, [resolver]);
|
}, [resolver]);
|
||||||
|
|
||||||
const handleOpenChange = useCallback((open: boolean) => {
|
const handleOpenChange = useCallback((open: boolean) => {
|
||||||
@@ -34,6 +42,9 @@ export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
|||||||
setResolver(null);
|
setResolver(null);
|
||||||
}
|
}
|
||||||
setIsOpen(open);
|
setIsOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setDialogOptions({});
|
||||||
|
}
|
||||||
}, [resolver]);
|
}, [resolver]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,6 +54,8 @@ export function FileBrowserProvider({ children }: { children: ReactNode }) {
|
|||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
title={dialogOptions.title}
|
||||||
|
description={dialogOptions.description}
|
||||||
/>
|
/>
|
||||||
</FileBrowserContext.Provider>
|
</FileBrowserContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -57,12 +70,15 @@ export function useFileBrowser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global reference for non-React code (like HttpApiClient)
|
// Global reference for non-React code (like HttpApiClient)
|
||||||
let globalFileBrowserFn: (() => Promise<string | null>) | null = null;
|
let globalFileBrowserFn: ((options?: FileBrowserOptions) => Promise<string | null>) | null = null;
|
||||||
|
|
||||||
export function setGlobalFileBrowser(fn: () => Promise<string | null>) {
|
export function setGlobalFileBrowser(fn: (options?: FileBrowserOptions) => Promise<string | null>) {
|
||||||
globalFileBrowserFn = fn;
|
globalFileBrowserFn = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGlobalFileBrowser() {
|
export function getGlobalFileBrowser() {
|
||||||
return globalFileBrowserFn;
|
return globalFileBrowserFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export the options type for consumers
|
||||||
|
export type { FileBrowserOptions };
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ export interface Feature {
|
|||||||
// Worktree info - set when a feature is being worked on in an isolated git worktree
|
// Worktree info - set when a feature is being worked on in an isolated git worktree
|
||||||
worktreePath?: string; // Path to the worktree directory
|
worktreePath?: string; // Path to the worktree directory
|
||||||
branchName?: string; // Name of the feature branch
|
branchName?: string; // Name of the feature branch
|
||||||
|
justFinished?: boolean; // Set to true when agent just finished and moved to waiting_approval
|
||||||
}
|
}
|
||||||
|
|
||||||
// File tree node for project analysis
|
// File tree node for project analysis
|
||||||
@@ -332,6 +333,13 @@ export interface AppState {
|
|||||||
// Project Analysis
|
// Project Analysis
|
||||||
projectAnalysis: ProjectAnalysis | null;
|
projectAnalysis: ProjectAnalysis | null;
|
||||||
isAnalyzing: boolean;
|
isAnalyzing: boolean;
|
||||||
|
|
||||||
|
// Board Background Settings (per-project, keyed by project path)
|
||||||
|
boardBackgroundByProject: Record<string, {
|
||||||
|
imagePath: string | null; // Path to background image in .automaker directory
|
||||||
|
cardOpacity: number; // Opacity of cards (0-100)
|
||||||
|
columnOpacity: number; // Opacity of columns (0-100)
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoModeActivity {
|
export interface AutoModeActivity {
|
||||||
@@ -455,6 +463,13 @@ export interface AppActions {
|
|||||||
setLastSelectedSession: (projectPath: string, sessionId: string | null) => void;
|
setLastSelectedSession: (projectPath: string, sessionId: string | null) => void;
|
||||||
getLastSelectedSession: (projectPath: string) => string | null;
|
getLastSelectedSession: (projectPath: string) => string | null;
|
||||||
|
|
||||||
|
// Board Background actions
|
||||||
|
setBoardBackground: (projectPath: string, imagePath: string | null) => void;
|
||||||
|
setCardOpacity: (projectPath: string, opacity: number) => void;
|
||||||
|
setColumnOpacity: (projectPath: string, opacity: number) => void;
|
||||||
|
getBoardBackground: (projectPath: string) => { imagePath: string | null; cardOpacity: number; columnOpacity: number };
|
||||||
|
clearBoardBackground: (projectPath: string) => void;
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
@@ -546,6 +561,7 @@ const initialState: AppState = {
|
|||||||
aiProfiles: DEFAULT_AI_PROFILES,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
|
boardBackgroundByProject: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppState & AppActions>()(
|
export const useAppStore = create<AppState & AppActions>()(
|
||||||
@@ -1131,6 +1147,69 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
getLastSelectedSession: (projectPath) => {
|
getLastSelectedSession: (projectPath) => {
|
||||||
return get().lastSelectedSessionByProject[projectPath] || null;
|
return get().lastSelectedSessionByProject[projectPath] || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Board Background actions
|
||||||
|
setBoardBackground: (projectPath, imagePath) => {
|
||||||
|
const current = get().boardBackgroundByProject;
|
||||||
|
const existing = current[projectPath] || { imagePath: null, cardOpacity: 100, columnOpacity: 100 };
|
||||||
|
set({
|
||||||
|
boardBackgroundByProject: {
|
||||||
|
...current,
|
||||||
|
[projectPath]: {
|
||||||
|
...existing,
|
||||||
|
imagePath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setCardOpacity: (projectPath, opacity) => {
|
||||||
|
const current = get().boardBackgroundByProject;
|
||||||
|
const existing = current[projectPath] || { imagePath: null, cardOpacity: 100, columnOpacity: 100 };
|
||||||
|
set({
|
||||||
|
boardBackgroundByProject: {
|
||||||
|
...current,
|
||||||
|
[projectPath]: {
|
||||||
|
...existing,
|
||||||
|
cardOpacity: opacity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setColumnOpacity: (projectPath, opacity) => {
|
||||||
|
const current = get().boardBackgroundByProject;
|
||||||
|
const existing = current[projectPath] || { imagePath: null, cardOpacity: 100, columnOpacity: 100 };
|
||||||
|
set({
|
||||||
|
boardBackgroundByProject: {
|
||||||
|
...current,
|
||||||
|
[projectPath]: {
|
||||||
|
...existing,
|
||||||
|
columnOpacity: opacity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getBoardBackground: (projectPath) => {
|
||||||
|
const settings = get().boardBackgroundByProject[projectPath];
|
||||||
|
return settings || { imagePath: null, cardOpacity: 100, columnOpacity: 100 };
|
||||||
|
},
|
||||||
|
|
||||||
|
clearBoardBackground: (projectPath) => {
|
||||||
|
const current = get().boardBackgroundByProject;
|
||||||
|
set({
|
||||||
|
boardBackgroundByProject: {
|
||||||
|
...current,
|
||||||
|
[projectPath]: {
|
||||||
|
imagePath: null,
|
||||||
|
cardOpacity: 100,
|
||||||
|
columnOpacity: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => set(initialState),
|
reset: () => set(initialState),
|
||||||
}),
|
}),
|
||||||
@@ -1164,6 +1243,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
aiProfiles: state.aiProfiles,
|
aiProfiles: state.aiProfiles,
|
||||||
chatSessions: state.chatSessions,
|
chatSessions: state.chatSessions,
|
||||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||||
|
// Board background settings
|
||||||
|
boardBackgroundByProject: state.boardBackgroundByProject,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -500,6 +500,86 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save board background image to .automaker/board directory
|
||||||
|
router.post("/save-board-background", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { data, filename, mimeType, projectPath } = req.body as {
|
||||||
|
data: string;
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
projectPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data || !filename || !projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: "data, filename, and projectPath are required",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create .automaker/board directory if it doesn't exist
|
||||||
|
const boardDir = path.join(projectPath, ".automaker", "board");
|
||||||
|
await fs.mkdir(boardDir, { recursive: true });
|
||||||
|
|
||||||
|
// Decode base64 data (remove data URL prefix if present)
|
||||||
|
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
||||||
|
const buffer = Buffer.from(base64Data, "base64");
|
||||||
|
|
||||||
|
// Use a fixed filename for the board background (overwrite previous)
|
||||||
|
const ext = path.extname(filename) || ".png";
|
||||||
|
const uniqueFilename = `background${ext}`;
|
||||||
|
const filePath = path.join(boardDir, uniqueFilename);
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
await fs.writeFile(filePath, buffer);
|
||||||
|
|
||||||
|
// Add project path to allowed paths if not already
|
||||||
|
addAllowedPath(projectPath);
|
||||||
|
|
||||||
|
// Return the relative path for storage
|
||||||
|
const relativePath = `.automaker/board/${uniqueFilename}`;
|
||||||
|
res.json({ success: true, path: relativePath });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
res.status(500).json({ success: false, error: message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete board background image
|
||||||
|
router.post("/delete-board-background", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: "projectPath is required",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardDir = path.join(projectPath, ".automaker", "board");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to remove all files in the board directory
|
||||||
|
const files = await fs.readdir(boardDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith("background")) {
|
||||||
|
await fs.unlink(path.join(boardDir, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Directory may not exist, that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
res.status(500).json({ success: false, error: message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Browse directories for file picker
|
// Browse directories for file picker
|
||||||
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
||||||
router.post("/browse", async (req: Request, res: Response) => {
|
router.post("/browse", async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -3,32 +3,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, type Request, type Response } from "express";
|
import { Router, type Request, type Response } from "express";
|
||||||
import path from "path";
|
import type { AutoModeService } from "../services/auto-mode-service.js";
|
||||||
|
|
||||||
interface RunningAgent {
|
export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
|
||||||
featureId: string;
|
|
||||||
projectPath: string;
|
|
||||||
projectName: string;
|
|
||||||
isAutoMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In-memory tracking of running agents (shared with auto-mode service via reference)
|
|
||||||
const runningAgentsMap = new Map<string, RunningAgent>();
|
|
||||||
let autoLoopRunning = false;
|
|
||||||
|
|
||||||
export function createRunningAgentsRoutes(): Router {
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Get all running agents
|
// Get all running agents
|
||||||
router.get("/", async (_req: Request, res: Response) => {
|
router.get("/", async (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const runningAgents = Array.from(runningAgentsMap.values());
|
const runningAgents = autoModeService.getRunningAgents();
|
||||||
|
const status = autoModeService.getStatus();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
runningAgents,
|
runningAgents,
|
||||||
totalCount: runningAgents.length,
|
totalCount: runningAgents.length,
|
||||||
autoLoopRunning,
|
autoLoopRunning: status.autoLoopRunning,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -38,33 +28,3 @@ export function createRunningAgentsRoutes(): Router {
|
|||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export functions to update running agents from other services
|
|
||||||
export function registerRunningAgent(
|
|
||||||
featureId: string,
|
|
||||||
projectPath: string,
|
|
||||||
isAutoMode: boolean
|
|
||||||
): void {
|
|
||||||
runningAgentsMap.set(featureId, {
|
|
||||||
featureId,
|
|
||||||
projectPath,
|
|
||||||
projectName: path.basename(projectPath),
|
|
||||||
isAutoMode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregisterRunningAgent(featureId: string): void {
|
|
||||||
runningAgentsMap.delete(featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setAutoLoopRunning(running: boolean): void {
|
|
||||||
autoLoopRunning = running;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRunningAgentsCount(): number {
|
|
||||||
return runningAgentsMap.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAgentRunning(featureId: string): boolean {
|
|
||||||
return runningAgentsMap.has(featureId);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -563,6 +563,23 @@ Format your response as a structured markdown document.`;
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed info about all running agents
|
||||||
|
*/
|
||||||
|
getRunningAgents(): Array<{
|
||||||
|
featureId: string;
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
isAutoMode: boolean;
|
||||||
|
}> {
|
||||||
|
return Array.from(this.runningFeatures.values()).map((rf) => ({
|
||||||
|
featureId: rf.featureId,
|
||||||
|
projectPath: rf.projectPath,
|
||||||
|
projectName: path.basename(rf.projectPath),
|
||||||
|
isAutoMode: rf.isAutoMode,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|
||||||
private async setupWorktree(
|
private async setupWorktree(
|
||||||
@@ -639,6 +656,13 @@ Format your response as a structured markdown document.`;
|
|||||||
const feature = JSON.parse(data);
|
const feature = JSON.parse(data);
|
||||||
feature.status = status;
|
feature.status = status;
|
||||||
feature.updatedAt = new Date().toISOString();
|
feature.updatedAt = new Date().toISOString();
|
||||||
|
// Set justFinished flag when moving to waiting_approval (agent just completed)
|
||||||
|
if (status === "waiting_approval") {
|
||||||
|
feature.justFinished = true;
|
||||||
|
} else {
|
||||||
|
// Clear the flag when moving to other statuses
|
||||||
|
feature.justFinished = false;
|
||||||
|
}
|
||||||
await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
|
||||||
} catch {
|
} catch {
|
||||||
// Feature file may not exist
|
// Feature file may not exist
|
||||||
|
|||||||
Reference in New Issue
Block a user