Merge branch 'fs/ui' into removing-electron-features-build-api

Resolved conflict in http-api-client.ts by adopting the server-side
file browser dialog approach from fs/ui branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alec Koifman
2025-12-12 17:38:05 -05:00
16 changed files with 810 additions and 192 deletions

View File

@@ -605,7 +605,7 @@ You should:
4. Based on the user's project overview, create a comprehensive app specification
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
6. Use the XML template format provided
7. Write the specification to .automaker/app_spec.txt
7. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives**
When analyzing, look at:
- package.json, cargo.toml, requirements.txt or similar config files for tech stack
@@ -615,11 +615,17 @@ When analyzing, look at:
- API structures and patterns
You CAN and SHOULD modify:
- .automaker/app_spec.txt (this is your primary target)
- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename)
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.
You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt.
**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.`;
**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.
**CRITICAL FILE NAMING RULES:**
- The spec file MUST be named exactly \`app_spec.txt\`
- Do NOT create project-spec.md, spec.md, or any other filename
- Do NOT use markdown (.md) extension - use .txt
- The full path must be: \`.automaker/app_spec.txt\``;
}
/**
@@ -656,7 +662,11 @@ ${APP_SPEC_XML_TEMPLATE}
- **development_workflow**: Note any testing or development patterns
- **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built
4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
4. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\`
- The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name
- Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename
- Do NOT output the spec in your response - write it to the file
- Use the Write tool with path \`.automaker/app_spec.txt\`
**Guidelines:**
- Be comprehensive! Include ALL features needed for a complete application
@@ -665,8 +675,9 @@ ${APP_SPEC_XML_TEMPLATE}
- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually
- Consider user flows, error states, and edge cases when defining features
- Each phase should have multiple specific, actionable features
- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!**
Begin by exploring the project structure.`;
Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`;
}
/**
@@ -865,7 +876,7 @@ You should:
3. Understand the current architecture and patterns used
4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
6. Write the specification to .automaker/app_spec.txt
6. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives**
When analyzing, look at:
- package.json, cargo.toml, or similar config files for tech stack
@@ -878,9 +889,15 @@ When analyzing, look at:
Your task is ONLY to update the app_spec.txt file - feature files will be managed separately.
You CAN and SHOULD modify:
- .automaker/app_spec.txt (this is your primary target)
- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename)
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt.
**CRITICAL FILE NAMING RULES:**
- The spec file MUST be named exactly \`app_spec.txt\`
- Do NOT create project-spec.md, spec.md, or any other filename
- Do NOT use markdown (.md) extension - use .txt
- The full path must be: \`.automaker/app_spec.txt\``;
}
/**
@@ -909,37 +926,40 @@ ${projectDefinition}
- Think about user experience, error handling, edge cases, etc.
- Architecture Notes: Any important architectural decisions or patterns
3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
3. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\`
- The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name
- Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename
- Do NOT output the spec in your response - write it to the file
- Use the Write tool with path \`.automaker/app_spec.txt\`
**Format Guidelines for the Spec:**
**Format Guidelines for the Spec (use XML format in app_spec.txt):**
Use this general structure:
Use this XML structure inside app_spec.txt:
\`\`\`
# [App Name] - Application Specification
## Product Overview
[Description of what the app does and its purpose]
## Tech Stack
- Frontend: [frameworks, libraries]
- Backend: [frameworks, APIs]
- Database: [if applicable]
- Other: [other relevant tech]
## Features
### [Category 1]
- **[Feature Name]**: [Detailed description of the feature]
- **[Feature Name]**: [Detailed description]
...
### [Category 2]
- **[Feature Name]**: [Detailed description]
...
## Architecture Notes
[Any important architectural notes, patterns, or conventions]
\`\`\`xml
<project_specification>
<project_name>[App Name]</project_name>
<overview>
[Description of what the app does and its purpose]
</overview>
<technology_stack>
<frontend>[frameworks, libraries]</frontend>
<backend>[frameworks, APIs]</backend>
<database>[if applicable]</database>
</technology_stack>
<core_capabilities>
[List all the major capabilities]
</core_capabilities>
<implementation_roadmap>
<phase_1>[Foundation features]</phase_1>
<phase_2>[Core features]</phase_2>
<phase_3>[Polish features]</phase_3>
</implementation_roadmap>
</project_specification>
\`\`\`
**Remember:**
@@ -947,9 +967,9 @@ Use this general structure:
- Consider user flows, error states, loading states, etc.
- Include authentication, authorization if relevant
- Think about what would make this a polished, production-ready app
- The more detailed and complete the spec, the better
- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!**
Begin by exploring the project structure.`;
Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`;
}
/**

View File

@@ -15,8 +15,9 @@ import { RunningAgentsView } from "@/components/views/running-agents-view";
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI, isElectron } from "@/lib/electron";
import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context";
export default function Home() {
function HomeContent() {
const {
currentView,
setCurrentView,
@@ -27,6 +28,7 @@ export default function Home() {
const { isFirstRun, setupComplete } = useSetupStore();
const [isMounted, setIsMounted] = useState(false);
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
const { openFileBrowser } = useFileBrowser();
// Hidden streamer panel - opens with "\" key
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
@@ -79,6 +81,11 @@ export default function Home() {
setIsMounted(true);
}, []);
// Initialize global file browser for HttpApiClient
useEffect(() => {
setGlobalFileBrowser(openFileBrowser);
}, [openFileBrowser]);
// Check if this is first run and redirect to setup if needed
useEffect(() => {
console.log("[Setup Flow] Checking setup state:", {
@@ -236,3 +243,11 @@ export default function Home() {
</main>
);
}
export default function Home() {
return (
<FileBrowserProvider>
<HomeContent />
</FileBrowserProvider>
);
}

View File

@@ -0,0 +1,231 @@
"use client";
import { useState, useEffect } from "react";
import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
interface DirectoryEntry {
name: string;
path: string;
}
interface BrowseResult {
success: boolean;
currentPath: string;
parentPath: string | null;
directories: DirectoryEntry[];
drives?: string[];
error?: string;
}
interface FileBrowserDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (path: string) => void;
title?: string;
description?: string;
}
export function FileBrowserDialog({
open,
onOpenChange,
onSelect,
title = "Select Project Directory",
description = "Navigate to your project folder",
}: FileBrowserDialogProps) {
const [currentPath, setCurrentPath] = useState<string>("");
const [parentPath, setParentPath] = useState<string | null>(null);
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
const [drives, setDrives] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const browseDirectory = async (dirPath?: string) => {
setLoading(true);
setError("");
try {
// Get server URL from environment or default
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
const response = await fetch(`${serverUrl}/api/fs/browse`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dirPath }),
});
const result: BrowseResult = await response.json();
if (result.success) {
setCurrentPath(result.currentPath);
setParentPath(result.parentPath);
setDirectories(result.directories);
setDrives(result.drives || []);
} else {
setError(result.error || "Failed to browse directory");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load directories");
} finally {
setLoading(false);
}
};
// Load home directory on mount
useEffect(() => {
if (open && !currentPath) {
browseDirectory();
}
}, [open]);
const handleSelectDirectory = (dir: DirectoryEntry) => {
browseDirectory(dir.path);
};
const handleGoToParent = () => {
if (parentPath) {
browseDirectory(parentPath);
}
};
const handleGoHome = () => {
browseDirectory();
};
const handleSelectDrive = (drivePath: string) => {
browseDirectory(drivePath);
};
const handleSelect = () => {
if (currentPath) {
onSelect(currentPath);
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderOpen className="w-5 h-5 text-brand-500" />
{title}
</DialogTitle>
<DialogDescription className="text-muted-foreground">
{description}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 min-h-[400px]">
{/* Drives selector (Windows only) */}
{drives.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-2">
<HardDrive className="w-3 h-3" />
<span>Drives:</span>
</div>
{drives.map((drive) => (
<Button
key={drive}
variant={currentPath.startsWith(drive) ? "default" : "outline"}
size="sm"
onClick={() => handleSelectDrive(drive)}
className="h-7 px-3 text-xs"
disabled={loading}
>
{drive.replace("\\", "")}
</Button>
))}
</div>
)}
{/* Current path breadcrumb */}
<div className="flex items-center gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<Button
variant="ghost"
size="sm"
onClick={handleGoHome}
className="h-7 px-2"
disabled={loading}
>
<Home className="w-4 h-4" />
</Button>
{parentPath && (
<Button
variant="ghost"
size="sm"
onClick={handleGoToParent}
className="h-7 px-2"
disabled={loading}
>
<ArrowLeft className="w-4 h-4" />
</Button>
)}
<div className="flex-1 font-mono text-sm truncate text-muted-foreground">
{currentPath || "Loading..."}
</div>
</div>
{/* Directory list */}
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
{loading && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-muted-foreground">Loading directories...</div>
</div>
)}
{error && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-destructive">{error}</div>
</div>
)}
{!loading && !error && directories.length === 0 && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-muted-foreground">No subdirectories found</div>
</div>
)}
{!loading && !error && directories.length > 0 && (
<div className="divide-y divide-sidebar-border">
{directories.map((dir) => (
<button
key={dir.path}
onClick={() => handleSelectDirectory(dir)}
className="w-full flex items-center gap-3 p-3 hover:bg-sidebar-accent/10 transition-colors text-left group"
>
<Folder className="w-5 h-5 text-brand-500 shrink-0" />
<span className="flex-1 truncate text-sm">{dir.name}</span>
<ChevronRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</button>
))}
</div>
)}
</div>
<div className="text-xs text-muted-foreground">
Click on a folder to navigate. Select the current folder or navigate to a subfolder.
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSelect} disabled={!currentPath || loading}>
<FolderOpen className="w-4 h-4 mr-2" />
Select Current Folder
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -399,7 +399,7 @@ ${Object.entries(projectAnalysis.filesByExtension)
`;
// Write the spec file
const specPath = `${currentProject.path}/app_spec.txt`;
const specPath = `${currentProject.path}/.automaker/app_spec.txt`;
const writeResult = await api.writeFile(specPath, specContent);
if (writeResult.success) {

View File

@@ -207,10 +207,12 @@ export const KanbanCard = memo(function KanbanCard({
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
// - verified items can always be dragged (to allow moving back to waiting_approval or backlog)
// - Non-skipTests (TDD) items in progress cannot be dragged (they are running)
const isDraggable =
feature.status === "backlog" ||
feature.status === "waiting_approval" ||
feature.status === "verified" ||
(feature.skipTests && !isCurrentAutoTask);
const {
attributes,

View File

@@ -61,13 +61,15 @@ export function AuthenticationStatusDisplay({
{claudeAuthStatus.method === "oauth_token_env"
? "Using CLAUDE_CODE_OAUTH_TOKEN"
: claudeAuthStatus.method === "oauth_token"
? "Using stored OAuth token (claude login)"
? "Using stored OAuth token (subscription)"
: claudeAuthStatus.method === "api_key_env"
? "Using ANTHROPIC_API_KEY"
: claudeAuthStatus.method === "api_key"
? "Using stored API key"
: claudeAuthStatus.method === "credentials_file"
? "Using credentials file"
: claudeAuthStatus.method === "cli_authenticated"
? "Using Claude CLI authentication"
: `Using ${claudeAuthStatus.method || "detected"} authentication`}
</span>
</div>

View File

@@ -74,8 +74,8 @@ export function useCliStatus() {
apiKeyValid?: boolean;
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, none
const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "none"] as const;
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "cli_authenticated", "none"] as const;
type AuthMethod = typeof validMethods[number];
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
? (auth.method as AuthMethod)

View File

@@ -40,6 +40,8 @@ export function useCliStatus({
"oauth_token",
"api_key",
"api_key_env",
"credentials_file",
"cli_authenticated",
"none",
] as const;
type AuthMethod = (typeof validMethods)[number];

View File

@@ -14,7 +14,8 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus } from "lucide-react";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus, CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
import { Checkbox } from "@/components/ui/checkbox";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import type { SpecRegenerationEvent } from "@/types/electron";
@@ -311,14 +312,22 @@ export function SpecView() {
// The backend sends explicit signals for completion:
// 1. "All tasks completed" in the message
// 2. [Phase: complete] marker in logs
// 3. "Spec regeneration complete!" for regeneration
// 4. "Initial spec creation complete!" for creation without features
const isFinalCompletionMessage = event.message?.includes("All tasks completed") ||
event.message === "All tasks completed!" ||
event.message === "All tasks completed";
event.message === "All tasks completed" ||
event.message === "Spec regeneration complete!" ||
event.message === "Initial spec creation complete!";
const hasCompletePhase = logsRef.current.includes("[Phase: complete]");
// Intermediate completion means features are being generated after spec creation
const isIntermediateCompletion = event.message?.includes("Features are being generated") ||
event.message?.includes("features are being generated");
// Rely solely on explicit backend signals
const shouldComplete = isFinalCompletionMessage || hasCompletePhase;
const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion;
if (shouldComplete) {
// Fully complete - clear all states immediately
@@ -337,9 +346,29 @@ export function SpecView() {
setProjectOverview("");
setErrorMessage("");
stateRestoredRef.current = false;
// Reload the spec to show the new content
loadSpec();
} else {
// Reload the spec with delay to ensure file is written to disk
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
// Show success toast notification
const isRegeneration = event.message?.includes("regeneration");
const isFeatureGeneration = event.message?.includes("Feature generation");
toast.success(
isFeatureGeneration
? "Feature Generation Complete"
: isRegeneration
? "Spec Regeneration Complete"
: "Spec Creation Complete",
{
description: isFeatureGeneration
? "Features have been created from the app specification."
: "Your app specification has been saved.",
icon: <CheckCircle2 className="w-4 h-4" />,
}
);
} else if (isIntermediateCompletion) {
// Intermediate completion - keep state active for feature generation
setIsCreating(true);
setIsRegenerating(true);

View File

@@ -0,0 +1,68 @@
"use client";
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog";
interface FileBrowserContextValue {
openFileBrowser: () => Promise<string | null>;
}
const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);
export function FileBrowserProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [resolver, setResolver] = useState<((value: string | null) => void) | null>(null);
const openFileBrowser = useCallback((): Promise<string | null> => {
return new Promise((resolve) => {
setIsOpen(true);
setResolver(() => resolve);
});
}, []);
const handleSelect = useCallback((path: string) => {
if (resolver) {
resolver(path);
setResolver(null);
}
setIsOpen(false);
}, [resolver]);
const handleOpenChange = useCallback((open: boolean) => {
if (!open && resolver) {
resolver(null);
setResolver(null);
}
setIsOpen(open);
}, [resolver]);
return (
<FileBrowserContext.Provider value={{ openFileBrowser }}>
{children}
<FileBrowserDialog
open={isOpen}
onOpenChange={handleOpenChange}
onSelect={handleSelect}
/>
</FileBrowserContext.Provider>
);
}
export function useFileBrowser() {
const context = useContext(FileBrowserContext);
if (!context) {
throw new Error("useFileBrowser must be used within FileBrowserProvider");
}
return context;
}
// Global reference for non-React code (like HttpApiClient)
let globalFileBrowserFn: (() => Promise<string | null>) | null = null;
export function setGlobalFileBrowser(fn: () => Promise<string | null>) {
globalFileBrowserFn = fn;
}
export function getGlobalFileBrowser() {
return globalFileBrowserFn;
}

View File

@@ -31,7 +31,7 @@ import type {
ModelDefinition,
ProviderStatus,
} from "@/types/electron";
import { openDirectoryPicker, openFilePicker, type DirectoryPickerResult } from "./file-picker";
import { getGlobalFileBrowser } from "@/contexts/file-browser-context";
// Server URL - configurable via environment variable
@@ -202,96 +202,62 @@ export class HttpApiClient implements ElectronAPI {
return { success: true };
}
// File picker - uses web-based file picker (works on Windows)
// File picker - uses server-side file browser dialog
async openDirectory(): Promise<DialogResult> {
try {
console.log("[HttpApiClient] Opening directory picker...");
const directoryInfo = await openDirectoryPicker();
console.log("[HttpApiClient] Directory info:", directoryInfo);
if (!directoryInfo) {
console.log("[HttpApiClient] No directory selected (user canceled)");
return { canceled: true, filePaths: [] };
}
const fileBrowser = getGlobalFileBrowser();
// Try to resolve directory path using server endpoint
// First, try if we have an absolute path (from file.path property)
if (directoryInfo.directoryName && (directoryInfo.directoryName.includes("\\") || directoryInfo.directoryName.includes("/") || directoryInfo.directoryName.startsWith("/"))) {
// Looks like an absolute path, try validating it directly
console.log("[HttpApiClient] Attempting direct path validation:", directoryInfo.directoryName);
const directResult = await this.post<{
success: boolean;
path?: string;
error?: string;
}>("/api/fs/validate-path", { filePath: directoryInfo.directoryName });
if (directResult.success && directResult.path) {
console.log("[HttpApiClient] Direct path validation succeeded:", directResult.path);
return { canceled: false, filePaths: [directResult.path] };
}
}
// If direct validation failed or we only have a directory name,
// use the resolve endpoint with directory structure
console.log("[HttpApiClient] Resolving directory using structure info...");
const result = await this.post<{
success: boolean;
path?: string;
error?: string;
}>("/api/fs/resolve-directory", {
directoryName: directoryInfo.directoryName,
sampleFiles: directoryInfo.sampleFiles,
fileCount: directoryInfo.fileCount,
});
console.log("[HttpApiClient] Directory resolution result:", result);
if (result.success && result.path) {
console.log("[HttpApiClient] Directory resolved successfully:", result.path);
return { canceled: false, filePaths: [result.path] };
}
// If resolution failed, show error
console.warn("[HttpApiClient] Directory resolution failed:", result.error);
const errorMsg = result.error || "Could not locate directory. Please ensure the directory exists and try selecting it again.";
alert(errorMsg);
return { canceled: true, filePaths: [] };
} catch (error) {
console.error("[HttpApiClient] Failed to open directory picker:", error);
alert("Failed to open directory picker. Please try again.");
if (!fileBrowser) {
console.error("File browser not initialized");
return { canceled: true, filePaths: [] };
}
const path = await fileBrowser();
if (!path) {
return { canceled: true, filePaths: [] };
}
// Validate with server
const result = await this.post<{
success: boolean;
path?: string;
error?: string;
}>("/api/fs/validate-path", { filePath: path });
if (result.success && result.path) {
return { canceled: false, filePaths: [result.path] };
}
console.error("Invalid directory:", result.error);
return { canceled: true, filePaths: [] };
}
async openFile(options?: object): Promise<DialogResult> {
try {
const selectedPath = await openFilePicker(options);
if (!selectedPath) {
return { canceled: true, filePaths: [] };
}
const fileBrowser = getGlobalFileBrowser();
// Handle both single file and multiple files
const filePaths = Array.isArray(selectedPath) ? selectedPath : [selectedPath];
// Validate files exist with server
// For multiple files, check the first one as a validation step
const firstPath = filePaths[0];
const result = await this.post<{ success: boolean; exists: boolean }>(
"/api/fs/exists",
{ filePath: firstPath }
);
if (result.success && result.exists) {
return { canceled: false, filePaths };
}
alert("File does not exist or cannot be accessed.");
return { canceled: true, filePaths: [] };
} catch (error) {
console.error("[HttpApiClient] Failed to open file picker:", error);
alert("Failed to open file picker. Please try again.");
if (!fileBrowser) {
console.error("File browser not initialized");
return { canceled: true, filePaths: [] };
}
// For now, use the same directory browser (could be enhanced for file selection)
const path = await fileBrowser();
if (!path) {
return { canceled: true, filePaths: [] };
}
const result = await this.post<{ success: boolean; exists: boolean }>(
"/api/fs/exists",
{ filePath: path }
);
if (result.success && result.exists) {
return { canceled: false, filePaths: [path] };
}
console.error("File not found");
return { canceled: true, filePaths: [] };
}
// File system operations

View File

@@ -17,6 +17,7 @@ export type ClaudeAuthMethod =
| "api_key_env" // ANTHROPIC_API_KEY environment variable
| "api_key" // Manually stored API key
| "credentials_file" // Generic credentials file detection
| "cli_authenticated" // Claude CLI is installed and has active sessions/activity
| "none";
// Claude Auth Status