Merge branch 'main' into fix/task-execution

This commit is contained in:
Shirone
2025-12-26 00:50:11 +01:00
committed by GitHub
33 changed files with 1394 additions and 234 deletions

3
.gitignore vendored
View File

@@ -79,4 +79,5 @@ blob-report/
# Misc
*.pem
docker-compose.override.yml
docker-compose.override.yml
.claude/

414
README.md
View File

@@ -19,7 +19,7 @@
- [What Makes Automaker Different?](#what-makes-automaker-different)
- [The Workflow](#the-workflow)
- [Powered by Claude Code](#powered-by-claude-code)
- [Powered by Claude Agent SDK](#powered-by-claude-agent-sdk)
- [Why This Matters](#why-this-matters)
- [Security Disclaimer](#security-disclaimer)
- [Community & Support](#community--support)
@@ -28,22 +28,36 @@
- [Quick Start](#quick-start)
- [How to Run](#how-to-run)
- [Development Mode](#development-mode)
- [Electron Desktop App (Recommended)](#electron-desktop-app-recommended)
- [Web Browser Mode](#web-browser-mode)
- [Building for Production](#building-for-production)
- [Running Production Build](#running-production-build)
- [Testing](#testing)
- [Linting](#linting)
- [Authentication Options](#authentication-options)
- [Persistent Setup (Optional)](#persistent-setup-optional)
- [Environment Configuration](#environment-configuration)
- [Authentication Setup](#authentication-setup)
- [Features](#features)
- [Core Workflow](#core-workflow)
- [AI & Planning](#ai--planning)
- [Project Management](#project-management)
- [Collaboration & Review](#collaboration--review)
- [Developer Tools](#developer-tools)
- [Advanced Features](#advanced-features)
- [Tech Stack](#tech-stack)
- [Frontend](#frontend)
- [Backend](#backend)
- [Testing & Quality](#testing--quality)
- [Shared Libraries](#shared-libraries)
- [Available Views](#available-views)
- [Architecture](#architecture)
- [Monorepo Structure](#monorepo-structure)
- [How It Works](#how-it-works)
- [Key Architectural Patterns](#key-architectural-patterns)
- [Security & Isolation](#security--isolation)
- [Data Storage](#data-storage)
- [Learn More](#learn-more)
- [License](#license)
</details>
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them.
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Agent SDK automatically implement them. Built with React, Vite, Electron, and Express, Automaker provides a complete workflow for managing AI agents through a desktop application (or web browser), with features like real-time streaming, git worktree isolation, plan approval, and multi-agent task execution.
![Automaker UI](https://i.imgur.com/jdwKydM.png)
@@ -59,9 +73,9 @@ Traditional development tools help you write code. Automaker helps you **orchest
4. **Review & Verify** - Review the changes, run tests, and approve when ready
5. **Ship Faster** - Build entire applications in days, not weeks
### Powered by Claude Code
### Powered by Claude Agent SDK
Automaker leverages the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe.
Automaker leverages the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe. The SDK provides autonomous AI agents that can use tools, make decisions, and complete complex multi-step tasks without constant human intervention.
### Why This Matters
@@ -95,8 +109,7 @@ In the Discord, you can:
- 🚀 Show off projects built with AI agents
- 🤝 Collaborate with other developers and contributors
👉 **Join the Discord:**
https://discord.gg/jjem7aEDKU
👉 **Join the Discord:** [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU)
---
@@ -104,28 +117,49 @@ https://discord.gg/jjem7aEDKU
### Prerequisites
- Node.js 18+
- npm
- [Claude Code CLI](https://code.claude.com/docs/en/overview) installed and authenticated
- **Node.js 18+** (tested with Node.js 22)
- **npm** (comes with Node.js)
- **Authentication** (choose one):
- **[Claude Code CLI](https://code.claude.com/docs/en/overview)** (recommended) - Install and authenticate, credentials used automatically
- **Anthropic API Key** - Direct API key for Claude Agent SDK ([get one here](https://console.anthropic.com/))
### Quick Start
```bash
# 1. Clone the repo
# 1. Clone the repository
git clone https://github.com/AutoMaker-Org/automaker.git
cd automaker
# 2. Install dependencies
npm install
# 3. Build local shared packages
# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly)
npm run build:packages
# 4. Run Automaker (pick your mode)
# 4. Set up authentication (skip if using Claude Code CLI)
# If using Claude Code CLI: credentials are detected automatically
# If using API key directly, choose one method:
# Option A: Environment variable
export ANTHROPIC_API_KEY="sk-ant-..."
# Option B: Create .env file in project root
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
# 5. Start Automaker (interactive launcher)
npm run dev
# Then choose your run mode when prompted, or use specific commands below
# Choose between:
# 1. Web Application (browser at localhost:3007)
# 2. Desktop Application (Electron - recommended)
```
**Note:** The `npm run dev` command will:
- Check for dependencies and install if needed
- Install Playwright browsers for E2E tests
- Kill any processes on ports 3007/3008
- Present an interactive menu to choose your run mode
## How to Run
### Development Mode
@@ -163,31 +197,65 @@ npm run dev:web
### Building for Production
#### Web Application
```bash
# Build Next.js app
# Build for web deployment (uses Vite)
npm run build
# Build Electron app for distribution
npm run build:electron
# Run production build
npm run start
```
### Running Production Build
#### Desktop Application
```bash
# Start production Next.js server
npm run start
# Build for current platform (macOS/Windows/Linux)
npm run build:electron
# Platform-specific builds
npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64)
npm run build:electron:win # Windows (NSIS installer, x64)
npm run build:electron:linux # Linux (AppImage + DEB, x64)
# Output directory: apps/ui/release/
```
#### Docker Deployment
```bash
# Build and run with Docker Compose (recommended for security)
docker-compose up -d
# Access at http://localhost:3007
# API at http://localhost:3008
```
### Testing
```bash
# Run tests headless
npm run test
#### End-to-End Tests (Playwright)
# Run tests with browser visible
npm run test:headed
```bash
npm run test # Headless E2E tests
npm run test:headed # Browser visible E2E tests
```
#### Unit Tests (Vitest)
```bash
npm run test:server # Server unit tests
npm run test:server:coverage # Server tests with coverage
npm run test:packages # All shared package tests
npm run test:all # Packages + server tests
```
#### Test Configuration
- E2E tests run on ports 3007 (UI) and 3008 (server)
- Automatically starts test servers before running
- Uses Chromium browser via Playwright
- Mock agent mode available in CI with `AUTOMAKER_MOCK_AGENT=true`
### Linting
```bash
@@ -195,59 +263,283 @@ npm run test:headed
npm run lint
```
### Authentication Options
### Environment Configuration
Automaker supports multiple authentication methods (in order of priority):
#### Authentication (if not using Claude Code CLI)
| Method | Environment Variable | Description |
| ---------------- | -------------------- | ------------------------------- |
| API Key (env) | `ANTHROPIC_API_KEY` | Anthropic API key |
| API Key (stored) | — | Anthropic API key stored in app |
- `ANTHROPIC_API_KEY` - Your Anthropic API key for Claude Agent SDK (not needed if using Claude Code CLI)
### Persistent Setup (Optional)
#### Optional - Server
- `PORT` - Server port (default: 3008)
- `DATA_DIR` - Data storage directory (default: ./data)
- `ENABLE_REQUEST_LOGGING` - HTTP request logging (default: true)
#### Optional - Security
- `AUTOMAKER_API_KEY` - Optional API authentication for the server
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
- `CORS_ORIGIN` - CORS policy (default: \*)
#### Optional - Development
- `VITE_SKIP_ELECTRON` - Skip Electron in dev mode
- `OPEN_DEVTOOLS` - Auto-open DevTools in Electron
### Authentication Setup
#### Option 1: Claude Code CLI (Recommended)
Install and authenticate the Claude Code CLI following the [official quickstart guide](https://code.claude.com/docs/en/quickstart).
Once authenticated, Automaker will automatically detect and use your CLI credentials. No additional configuration needed!
#### Option 2: Direct API Key
If you prefer not to use the CLI, you can provide an Anthropic API key directly using one of these methods:
##### 2a. Shell Configuration
Add to your `~/.bashrc` or `~/.zshrc`:
```bash
export ANTHROPIC_API_KEY="YOUR_API_KEY_HERE"
export ANTHROPIC_API_KEY="sk-ant-..."
```
Then restart your terminal or run `source ~/.bashrc`.
Then restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`).
##### 2b. .env File
Create a `.env` file in the project root (gitignored):
```bash
ANTHROPIC_API_KEY=sk-ant-...
PORT=3008
DATA_DIR=./data
```
##### 2c. In-App Storage
The application can store your API key securely in the settings UI. The key is persisted in the `DATA_DIR` directory.
## Features
### Core Workflow
- 📋 **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages
- 🤖 **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress"
- 🧠 **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more
- 💭 **Extended Thinking** - Enable extended thinking modes for complex problem-solving
- 📡 **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented
- 🔍 **Project Analysis** - AI-powered project structure analysis to understand your codebase
- 📁 **Context Management** - Add context files to help AI agents understand your project better
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on your project
- 🖼️ **Image Support** - Attach images and screenshots to feature descriptions
- **Concurrent Processing** - Configure concurrency to process multiple features simultaneously
- 🧪 **Test Integration** - Automatic test running and verification for implemented features
- 🔀 **Git Integration** - View git diffs and track changes made by AI agents
- 👤 **AI Profiles** - Create and manage different AI agent profiles for various tasks
- 💬 **Chat History** - Keep track of conversations and interactions with AI agents
- ⌨️ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts
- 🎨 **Dark/Light Theme** - Beautiful UI with theme support
- 🖥️ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux
- 🔀 **Git Worktree Isolation** - Each feature executes in isolated git worktrees to protect your main branch
- 📡 **Real-time Streaming** - Watch AI agents work in real-time with live tool usage, progress updates, and task completion
- 🔄 **Follow-up Instructions** - Send additional instructions to running agents without stopping them
### AI & Planning
- 🧠 **Multi-Model Support** - Choose from Claude Opus, Sonnet, and Haiku per feature
- 💭 **Extended Thinking** - Enable thinking modes (none, medium, deep, ultra) for complex problem-solving
- 📝 **Planning Modes** - Four planning levels: skip (direct implementation), lite (quick plan), spec (task breakdown), full (phased execution)
- **Plan Approval** - Review and approve AI-generated plans before implementation begins
- 📊 **Multi-Agent Task Execution** - Spec mode spawns dedicated agents per task for focused implementation
### Project Management
- 🔍 **Project Analysis** - AI-powered codebase analysis to understand your project structure
- 💡 **Feature Suggestions** - AI-generated feature suggestions based on project analysis
- 📁 **Context Management** - Add markdown, images, and documentation files that agents automatically reference
- 🔗 **Dependency Blocking** - Features can depend on other features, enforcing execution order
- 🌳 **Graph View** - Visualize feature dependencies with interactive graph visualization
- 📋 **GitHub Integration** - Import issues, validate feasibility, and convert to tasks automatically
### Collaboration & Review
- 🧪 **Verification Workflow** - Features move to "Waiting Approval" for review and testing
- 💬 **Agent Chat** - Interactive chat sessions with AI agents for exploratory work
- 👤 **AI Profiles** - Create custom agent configurations with different prompts, models, and settings
- 📜 **Session History** - Persistent chat sessions across restarts with full conversation history
- 🔍 **Git Diff Viewer** - Review changes made by agents before approving
### Developer Tools
- 🖥️ **Integrated Terminal** - Full terminal access with tabs, splits, and persistent sessions
- 🖼️ **Image Support** - Attach screenshots and diagrams to feature descriptions for visual context
-**Concurrent Execution** - Configure how many features can run simultaneously (default: 3)
- ⌨️ **Keyboard Shortcuts** - Fully customizable shortcuts for navigation and actions
- 🎨 **Theme System** - 25+ themes including Dark, Light, Dracula, Nord, Catppuccin, and more
- 🖥️ **Cross-Platform** - Desktop app for macOS (x64, arm64), Windows (x64), and Linux (x64)
- 🌐 **Web Mode** - Run in browser or as Electron desktop app
### Advanced Features
- 🔐 **Docker Isolation** - Security-focused Docker deployment with no host filesystem access
- 🎯 **Worktree Management** - Create, switch, commit, and create PRs from worktrees
- 📊 **Usage Tracking** - Monitor Claude API usage with detailed metrics
- 🔊 **Audio Notifications** - Optional completion sounds (mutable in settings)
- 💾 **Auto-save** - All work automatically persisted to `.automaker/` directory
## Tech Stack
- [Next.js](https://nextjs.org) - React framework
- [Electron](https://www.electronjs.org/) - Desktop application framework
- [Tailwind CSS](https://tailwindcss.com/) - Styling
- [Zustand](https://zustand-demo.pmnd.rs/) - State management
- [dnd-kit](https://dndkit.com/) - Drag and drop functionality
### Frontend
- **React 19** - UI framework
- **Vite 7** - Build tool and development server
- **Electron 39** - Desktop application framework
- **TypeScript 5.9** - Type safety
- **TanStack Router** - File-based routing
- **Zustand 5** - State management with persistence
- **Tailwind CSS 4** - Utility-first styling with 25+ themes
- **Radix UI** - Accessible component primitives
- **dnd-kit** - Drag and drop for Kanban board
- **@xyflow/react** - Graph visualization for dependencies
- **xterm.js** - Integrated terminal emulator
- **CodeMirror 6** - Code editor for XML/syntax highlighting
- **Lucide Icons** - Icon library
### Backend
- **Node.js** - JavaScript runtime with ES modules
- **Express 5** - HTTP server framework
- **TypeScript 5.9** - Type safety
- **Claude Agent SDK** - AI agent integration (@anthropic-ai/claude-agent-sdk)
- **WebSocket (ws)** - Real-time event streaming
- **node-pty** - PTY terminal sessions
### Testing & Quality
- **Playwright** - End-to-end testing
- **Vitest** - Unit testing framework
- **ESLint 9** - Code linting
- **Prettier 3** - Code formatting
- **Husky** - Git hooks for pre-commit formatting
### Shared Libraries
- **@automaker/types** - Shared TypeScript definitions
- **@automaker/utils** - Logging, error handling, image processing
- **@automaker/prompts** - AI prompt templates
- **@automaker/platform** - Path management and security
- **@automaker/model-resolver** - Claude model alias resolution
- **@automaker/dependency-resolver** - Feature dependency ordering
- **@automaker/git-utils** - Git operations and worktree management
## Available Views
Automaker provides several specialized views accessible via the sidebar or keyboard shortcuts:
| View | Shortcut | Description |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------ |
| **Board** | `K` | Kanban board for managing feature workflow (Backlog → In Progress → Waiting Approval → Verified) |
| **Agent** | `A` | Interactive chat sessions with AI agents for exploratory work and questions |
| **Spec** | `D` | Project specification editor with AI-powered generation and feature suggestions |
| **Context** | `C` | Manage context files (markdown, images) that AI agents automatically reference |
| **Profiles** | `M` | Create and manage AI agent profiles with custom prompts and configurations |
| **Settings** | `S` | Configure themes, shortcuts, defaults, authentication, and more |
| **Terminal** | `T` | Integrated terminal with tabs, splits, and persistent sessions |
| **GitHub Issues** | - | Import and validate GitHub issues, convert to tasks |
| **Running Agents** | - | View all active agents across projects with status and progress |
### Keyboard Navigation
All shortcuts are customizable in Settings. Default shortcuts:
- **Navigation:** `K` (Board), `A` (Agent), `D` (Spec), `C` (Context), `S` (Settings), `M` (Profiles), `T` (Terminal)
- **UI:** `` ` `` (Toggle sidebar)
- **Actions:** `N` (New item in current view), `G` (Start next features), `O` (Open project), `P` (Project picker)
- **Projects:** `Q`/`E` (Cycle previous/next project)
## Architecture
### Monorepo Structure
Automaker is built as an npm workspace monorepo with two main applications and seven shared packages:
```text
automaker/
├── apps/
│ ├── ui/ # React + Vite + Electron frontend
│ └── server/ # Express + WebSocket backend
└── libs/ # Shared packages
├── types/ # Core TypeScript definitions
├── utils/ # Logging, errors, utilities
├── prompts/ # AI prompt templates
├── platform/ # Path management, security
├── model-resolver/ # Claude model aliasing
├── dependency-resolver/ # Feature dependency ordering
└── git-utils/ # Git operations & worktree management
```
### How It Works
1. **Feature Definition** - Users create feature cards on the Kanban board with descriptions, images, and configuration
2. **Git Worktree Creation** - When a feature starts, a git worktree is created for isolated development
3. **Agent Execution** - Claude Agent SDK executes in the worktree with full file system and command access
4. **Real-time Streaming** - Agent output streams via WebSocket to the frontend for live monitoring
5. **Plan Approval** (optional) - For spec/full planning modes, agents generate plans that require user approval
6. **Multi-Agent Tasks** (spec mode) - Each task in the spec gets a dedicated agent for focused implementation
7. **Verification** - Features move to "Waiting Approval" where changes can be reviewed via git diff
8. **Integration** - After approval, changes can be committed and PRs created from the worktree
### Key Architectural Patterns
- **Event-Driven Architecture** - All server operations emit events that stream to the frontend
- **Provider Pattern** - Extensible AI provider system (currently Claude, designed for future providers)
- **Service-Oriented Backend** - Modular services for agent management, features, terminals, settings
- **State Management** - Zustand with persistence for frontend state across restarts
- **File-Based Storage** - No database; features stored as JSON files in `.automaker/` directory
### Security & Isolation
- **Git Worktrees** - Each feature executes in an isolated git worktree, protecting your main branch
- **Path Sandboxing** - Optional `ALLOWED_ROOT_DIRECTORY` restricts file access
- **Docker Isolation** - Recommended deployment uses Docker with no host filesystem access
- **Plan Approval** - Optional plan review before implementation prevents unwanted changes
### Data Storage
Automaker uses a file-based storage system (no database required):
#### Per-Project Data
Stored in `{projectPath}/.automaker/`:
```text
.automaker/
├── features/ # Feature JSON files and images
│ └── {featureId}/
│ ├── feature.json # Feature metadata
│ ├── agent-output.md # AI agent output log
│ └── images/ # Attached images
├── context/ # Context files for AI agents
├── settings.json # Project-specific settings
├── spec.md # Project specification
├── analysis.json # Project structure analysis
└── feature-suggestions.json # AI-generated suggestions
```
#### Global Data
Stored in `DATA_DIR` (default `./data`):
```text
data/
├── settings.json # Global settings, profiles, shortcuts
├── credentials.json # API keys (encrypted)
├── sessions-metadata.json # Chat session metadata
└── agent-sessions/ # Conversation histories
└── {sessionId}.json
```
## Learn More
To learn more about Next.js, take a look at the following resources:
### Documentation
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs
- [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment
- [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages
### Community
Join the **Agentic Jumpstart** Discord to connect with other builders exploring **agentic coding**:
👉 [Agentic Jumpstart Discord](https://discord.gg/jjem7aEDKU)
## License

View File

@@ -112,10 +112,11 @@ app.use(express.json({ limit: '50mb' }));
const events: EventEmitter = createEventEmitter();
// Create services
const agentService = new AgentService(DATA_DIR, events);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events);
// Note: settingsService is created first so it can be injected into other services
const settingsService = new SettingsService(DATA_DIR);
const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService();
// Initialize services
@@ -148,17 +149,17 @@ app.use('/api/enhance-prompt', createEnhancePromptRoutes());
app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes());
app.use('/api/suggestions', createSuggestionsRoutes(events));
app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService));
app.use('/api/models', createModelsRoutes());
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events));
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
app.use('/api/workspace', createWorkspaceRoutes());
app.use('/api/templates', createTemplatesRoutes());
app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/github', createGitHubRoutes(events));
app.use('/api/context', createContextRoutes());
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
// Create HTTP server
const server = createServer(app);

View File

@@ -136,6 +136,59 @@ function getBaseOptions(): Partial<Options> {
};
}
/**
* Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true:
* - Uses preset mode with 'claude_code' to enable CLAUDE.md auto-loading
* - If there's a custom systemPrompt, appends it to the preset
* - Sets settingSources to ['project'] for SDK to load CLAUDE.md files
*
* @param config - The SDK options config
* @returns Object with systemPrompt and settingSources for SDK options
*/
function buildClaudeMdOptions(config: CreateSdkOptionsConfig): {
systemPrompt?: string | SystemPromptConfig;
settingSources?: Array<'user' | 'project' | 'local'>;
} {
if (!config.autoLoadClaudeMd) {
// Standard mode - just pass through the system prompt as-is
return config.systemPrompt ? { systemPrompt: config.systemPrompt } : {};
}
// Auto-load CLAUDE.md mode - use preset with settingSources
const result: {
systemPrompt: SystemPromptConfig;
settingSources: Array<'user' | 'project' | 'local'>;
} = {
systemPrompt: {
type: 'preset',
preset: 'claude_code',
},
// Load both user (~/.claude/CLAUDE.md) and project (.claude/CLAUDE.md) settings
settingSources: ['user', 'project'],
};
// If there's a custom system prompt, append it to the preset
if (config.systemPrompt) {
result.systemPrompt.append = config.systemPrompt;
}
return result;
}
/**
* System prompt configuration for SDK options
* When using preset mode with claude_code, CLAUDE.md files are automatically loaded
*/
export interface SystemPromptConfig {
/** Use preset mode with claude_code to enable CLAUDE.md auto-loading */
type: 'preset';
/** The preset to use - 'claude_code' enables CLAUDE.md loading */
preset: 'claude_code';
/** Optional additional prompt to append to the preset */
append?: string;
}
/**
* Options configuration for creating SDK options
*/
@@ -160,6 +213,9 @@ export interface CreateSdkOptionsConfig {
type: 'json_schema';
schema: Record<string, unknown>;
};
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;
}
/**
@@ -169,11 +225,15 @@ export interface CreateSdkOptionsConfig {
* - Uses read-only tools for codebase analysis
* - Extended turns for thorough exploration
* - Opus model by default (can be overridden)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
// Override permissionMode - spec generation only needs read-only tools
@@ -184,7 +244,7 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.specGeneration],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
@@ -197,11 +257,15 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
* - Uses read-only tools (just needs to read the spec)
* - Quick turns since it's mostly JSON generation
* - Sonnet model by default for speed
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
// Override permissionMode - feature generation only needs read-only tools
@@ -210,7 +274,7 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
maxTurns: MAX_TURNS.quick,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -222,18 +286,22 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
* - Uses read-only tools for analysis
* - Standard turns to allow thorough codebase exploration and structured output generation
* - Opus model by default for thorough analysis
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('suggestions', config.model),
maxTurns: MAX_TURNS.extended,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
@@ -247,6 +315,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
* - Standard turns for interactive sessions
* - Model priority: explicit model > session model > chat default
* - Sandbox enabled for bash safety
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
@@ -255,6 +324,9 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Model priority: explicit model > session model > chat default
const effectiveModel = config.model || config.sessionModel;
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('chat', effectiveModel),
@@ -265,7 +337,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -278,11 +350,15 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Extended turns for thorough feature implementation
* - Uses default model (can be overridden)
* - Sandbox enabled for bash safety
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('auto', config.model),
@@ -293,7 +369,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
enabled: true,
autoAllowBashIfSandboxed: true,
},
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -302,6 +378,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
* Create custom SDK options with explicit configuration
*
* Use this when the preset options don't fit your use case.
* When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createCustomOptions(
config: CreateSdkOptionsConfig & {
@@ -313,6 +390,9 @@ export function createCustomOptions(
// Validate working directory before creating options
validateWorkingDirectory(config.cwd);
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('default', config.model),
@@ -320,7 +400,7 @@ export function createCustomOptions(
cwd: config.cwd,
allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly],
...(config.sandbox && { sandbox: config.sandbox }),
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
};
}

View File

@@ -0,0 +1,110 @@
/**
* Helper utilities for loading settings and context file handling across different parts of the server
*/
import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';
/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
* Returns false if settings service is not available.
*
* @param projectPath - Path to the project
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[DescribeImage]')
* @returns Promise resolving to the autoLoadClaudeMd setting value
*/
export async function getAutoLoadClaudeMdSetting(
projectPath: string,
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<boolean> {
if (!settingsService) {
console.log(`${logPrefix} SettingsService not available, autoLoadClaudeMd disabled`);
return false;
}
try {
// Check project settings first (takes precedence)
const projectSettings = await settingsService.getProjectSettings(projectPath);
if (projectSettings.autoLoadClaudeMd !== undefined) {
console.log(
`${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
);
return projectSettings.autoLoadClaudeMd;
}
// Fall back to global settings
const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.autoLoadClaudeMd ?? false;
console.log(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`);
return result;
} catch (error) {
console.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error);
throw error;
}
}
/**
* Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
* and rebuilds the formatted prompt without it.
*
* When autoLoadClaudeMd is true, the SDK handles CLAUDE.md loading via settingSources,
* so we need to exclude it from the manual context loading to avoid duplication.
* Other context files (CODE_QUALITY.md, CONVENTIONS.md, etc.) are preserved.
*
* @param contextResult - Result from loadContextFiles
* @param autoLoadClaudeMd - Whether SDK auto-loading is enabled
* @returns Filtered context prompt (empty string if no non-CLAUDE.md files)
*/
export function filterClaudeMdFromContext(
contextResult: ContextFilesResult,
autoLoadClaudeMd: boolean
): string {
// If autoLoadClaudeMd is disabled, return the original prompt unchanged
if (!autoLoadClaudeMd || contextResult.files.length === 0) {
return contextResult.formattedPrompt;
}
// Filter out CLAUDE.md (case-insensitive)
const nonClaudeFiles = contextResult.files.filter((f) => f.name.toLowerCase() !== 'claude.md');
// If all files were CLAUDE.md, return empty string
if (nonClaudeFiles.length === 0) {
return '';
}
// Rebuild prompt without CLAUDE.md using the same format as loadContextFiles
const formattedFiles = nonClaudeFiles.map((file) => formatContextFileEntry(file));
return `# Project Context Files
The following context files provide project-specific rules, conventions, and guidelines.
Each file serves a specific purpose - use the description to understand when to reference it.
If you need more details about a context file, you can read the full file at the path provided.
**IMPORTANT**: You MUST follow the rules and conventions specified in these files.
- Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`)
- Follow ALL coding conventions, commit message formats, and architectural patterns specified
- Reference these rules before running ANY shell commands or making commits
---
${formattedFiles.join('\n\n---\n\n')}
---
**REMINDER**: Before taking any action, verify you are following the conventions specified above.
`;
}
/**
* Format a single context file entry for the prompt
* (Matches the format used in @automaker/utils/context-loader.ts)
*/
function formatContextFileEntry(file: ContextFileInfo): string {
const header = `## ${file.name}`;
const pathInfo = `**Path:** \`${file.path}\``;
const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : '';
return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`;
}

View File

@@ -55,6 +55,8 @@ export class ClaudeProvider extends BaseProvider {
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
? { resume: sdkSessionId }
: {}),
// Forward settingSources for CLAUDE.md file loading
...(options.settingSources && { settingSources: options.settingSources }),
};
// Build prompt payload

View File

@@ -26,13 +26,14 @@ export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string;
systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string };
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load
}
/**

View File

@@ -10,6 +10,8 @@ import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
import { logAuthStatus } from './common.js';
import { parseAndCreateFeatures } from './parse-and-create-features.js';
import { getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -19,7 +21,8 @@ export async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController,
maxFeatures?: number
maxFeatures?: number,
settingsService?: SettingsService
): Promise<void> {
const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES;
logger.debug('========== generateFeaturesFromSpec() started ==========');
@@ -91,9 +94,17 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
projectPath: projectPath,
});
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[FeatureGeneration]'
);
const options = createFeatureGenerationOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2));

View File

@@ -17,6 +17,8 @@ import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
import { logAuthStatus } from './common.js';
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -27,7 +29,8 @@ export async function generateSpec(
abortController: AbortController,
generateFeatures?: boolean,
analyzeProject?: boolean,
maxFeatures?: number
maxFeatures?: number,
settingsService?: SettingsService
): Promise<void> {
logger.info('========== generateSpec() started ==========');
logger.info('projectPath:', projectPath);
@@ -83,9 +86,17 @@ ${getStructuredSpecPromptInstruction()}`;
content: 'Starting spec generation...\n',
});
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[SpecRegeneration]'
);
const options = createSpecGenerationOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
outputFormat: {
type: 'json_schema',
schema: specOutputSchema,
@@ -269,7 +280,13 @@ ${getStructuredSpecPromptInstruction()}`;
// Create a new abort controller for feature generation
const featureAbortController = new AbortController();
try {
await generateFeaturesFromSpec(projectPath, events, featureAbortController, maxFeatures);
await generateFeaturesFromSpec(
projectPath,
events,
featureAbortController,
maxFeatures,
settingsService
);
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
} catch (featureError) {
logger.error('Feature generation failed:', featureError);

View File

@@ -9,13 +9,17 @@ import { createGenerateHandler } from './routes/generate.js';
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
import type { SettingsService } from '../../services/settings-service.js';
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
export function createSpecRegenerationRoutes(
events: EventEmitter,
settingsService?: SettingsService
): Router {
const router = Router();
router.post('/create', createCreateHandler(events));
router.post('/generate', createGenerateHandler(events));
router.post('/generate-features', createGenerateFeaturesHandler(events));
router.post('/generate', createGenerateHandler(events, settingsService));
router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService));
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());

View File

@@ -13,10 +13,14 @@ import {
getErrorMessage,
} from '../common.js';
import { generateFeaturesFromSpec } from '../generate-features-from-spec.js';
import type { SettingsService } from '../../../services/settings-service.js';
const logger = createLogger('SpecRegeneration');
export function createGenerateFeaturesHandler(events: EventEmitter) {
export function createGenerateFeaturesHandler(
events: EventEmitter,
settingsService?: SettingsService
) {
return async (req: Request, res: Response): Promise<void> => {
logger.info('========== /generate-features endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
@@ -49,7 +53,7 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
setRunningState(true, abortController);
logger.info('Starting background feature generation task...');
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures)
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService)
.catch((error) => {
logError(error, 'Feature generation failed with error');
events.emit('spec-regeneration:event', {

View File

@@ -13,10 +13,11 @@ import {
getErrorMessage,
} from '../common.js';
import { generateSpec } from '../generate-spec.js';
import type { SettingsService } from '../../../services/settings-service.js';
const logger = createLogger('SpecRegeneration');
export function createGenerateHandler(events: EventEmitter) {
export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
logger.info('========== /generate endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
@@ -67,7 +68,8 @@ export function createGenerateHandler(events: EventEmitter) {
abortController,
generateFeatures,
analyzeProject,
maxFeatures
maxFeatures,
settingsService
)
.catch((error) => {
logError(error, 'Generation failed with error');

View File

@@ -8,17 +8,19 @@
import { Router } from 'express';
import { createDescribeImageHandler } from './routes/describe-image.js';
import { createDescribeFileHandler } from './routes/describe-file.js';
import type { SettingsService } from '../../services/settings-service.js';
/**
* Create the context router
*
* @param settingsService - Optional settings service for loading autoLoadClaudeMd setting
* @returns Express router with context endpoints
*/
export function createContextRoutes(): Router {
export function createContextRoutes(settingsService?: SettingsService): Router {
const router = Router();
router.post('/describe-image', createDescribeImageHandler());
router.post('/describe-file', createDescribeFileHandler());
router.post('/describe-image', createDescribeImageHandler(settingsService));
router.post('/describe-file', createDescribeFileHandler(settingsService));
return router;
}

View File

@@ -17,6 +17,8 @@ import { PathNotAllowedError } from '@automaker/platform';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeFile');
@@ -72,9 +74,12 @@ async function extractTextFromStream(
/**
* Create the describe-file request handler
*
* @param settingsService - Optional settings service for loading autoLoadClaudeMd setting
* @returns Express request handler for file description
*/
export function createDescribeFileHandler(): (req: Request, res: Response) => Promise<void> {
export function createDescribeFileHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { filePath } = req.body as DescribeFileRequestBody;
@@ -165,6 +170,13 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
// Use the file's directory as the working directory
const cwd = path.dirname(resolvedPath);
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
cwd,
settingsService,
'[DescribeFile]'
);
// Use centralized SDK options with proper cwd validation
// No tools needed since we're passing file content directly
const sdkOptions = createCustomOptions({
@@ -172,6 +184,7 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
model: CLAUDE_MODEL_MAP.haiku,
maxTurns: 1,
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
});

View File

@@ -17,6 +17,8 @@ import { CLAUDE_MODEL_MAP } from '@automaker/types';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import * as fs from 'fs';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeImage');
@@ -226,9 +228,12 @@ async function extractTextFromStream(
* Uses Claude SDK query with multi-part content blocks to include the image (base64),
* matching the agent runner behavior.
*
* @param settingsService - Optional settings service for loading autoLoadClaudeMd setting
* @returns Express request handler for image description
*/
export function createDescribeImageHandler(): (req: Request, res: Response) => Promise<void> {
export function createDescribeImageHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
const requestId = `describe-image-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const startedAt = Date.now();
@@ -325,12 +330,20 @@ export function createDescribeImageHandler(): (req: Request, res: Response) => P
const cwd = path.dirname(actualPath);
logger.info(`[${requestId}] Using cwd=${cwd}`);
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
cwd,
settingsService,
'[DescribeImage]'
);
// Use the same centralized option builder used across the server (validates cwd)
const sdkOptions = createCustomOptions({
cwd,
model: CLAUDE_MODEL_MAP.haiku,
maxTurns: 1,
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
});

View File

@@ -16,8 +16,12 @@ import {
createDeleteValidationHandler,
createMarkViewedHandler,
} from './routes/validation-endpoints.js';
import type { SettingsService } from '../../services/settings-service.js';
export function createGitHubRoutes(events: EventEmitter): Router {
export function createGitHubRoutes(
events: EventEmitter,
settingsService?: SettingsService
): Router {
const router = Router();
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
@@ -26,7 +30,7 @@ export function createGitHubRoutes(events: EventEmitter): Router {
router.post(
'/validate-issue',
validatePathParams('projectPath'),
createValidateIssueHandler(events)
createValidateIssueHandler(events, settingsService)
);
// Validation management endpoints

View File

@@ -23,6 +23,8 @@ import {
logError,
logger,
} from './validation-common.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
/** Valid model values for validation */
const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const;
@@ -54,7 +56,8 @@ async function runValidation(
issueLabels: string[] | undefined,
model: AgentModel,
events: EventEmitter,
abortController: AbortController
abortController: AbortController,
settingsService?: SettingsService
): Promise<void> {
// Emit start event
const startEvent: IssueValidationEvent = {
@@ -76,12 +79,20 @@ async function runValidation(
// Build the prompt
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[ValidateIssue]'
);
// Create SDK options with structured output and abort controller
const options = createSuggestionsOptions({
cwd: projectPath,
model,
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
abortController,
autoLoadClaudeMd,
outputFormat: {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,
@@ -190,7 +201,10 @@ async function runValidation(
* - System prompt guiding the validation process
* - Async execution with event emission
*/
export function createValidateIssueHandler(events: EventEmitter) {
export function createValidateIssueHandler(
events: EventEmitter,
settingsService?: SettingsService
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const {
@@ -256,7 +270,8 @@ export function createValidateIssueHandler(events: EventEmitter) {
issueLabels,
model,
events,
abortController
abortController,
settingsService
)
.catch((error) => {
// Error is already handled inside runValidation (event emitted)

View File

@@ -9,6 +9,8 @@ import { createSuggestionsOptions } from '../../lib/sdk-options.js';
import { FeatureLoader } from '../../services/feature-loader.js';
import { getAppSpecPath } from '@automaker/platform';
import * as secureFs from '../../lib/secure-fs.js';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
const logger = createLogger('Suggestions');
@@ -125,7 +127,8 @@ export async function generateSuggestions(
projectPath: string,
suggestionType: string,
events: EventEmitter,
abortController: AbortController
abortController: AbortController,
settingsService?: SettingsService
): Promise<void> {
const typePrompts: Record<string, string> = {
features: 'Analyze this project and suggest new features that would add value.',
@@ -154,9 +157,17 @@ The response will be automatically formatted as structured JSON.`;
// Don't send initial message - let the agent output speak for itself
// The first agent message will be captured as an info entry
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[Suggestions]'
);
const options = createSuggestionsOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
outputFormat: {
type: 'json_schema',
schema: suggestionsSchema,

View File

@@ -8,11 +8,19 @@ import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGenerateHandler } from './routes/generate.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
import type { SettingsService } from '../../services/settings-service.js';
export function createSuggestionsRoutes(events: EventEmitter): Router {
export function createSuggestionsRoutes(
events: EventEmitter,
settingsService?: SettingsService
): Router {
const router = Router();
router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events));
router.post(
'/generate',
validatePathParams('projectPath'),
createGenerateHandler(events, settingsService)
);
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());

View File

@@ -7,10 +7,11 @@ import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
import { generateSuggestions } from '../generate-suggestions.js';
import type { SettingsService } from '../../../services/settings-service.js';
const logger = createLogger('Suggestions');
export function createGenerateHandler(events: EventEmitter) {
export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, suggestionType = 'features' } = req.body as {
@@ -37,7 +38,7 @@ export function createGenerateHandler(events: EventEmitter) {
setRunningState(true, abortController);
// Start generation in background
generateSuggestions(projectPath, suggestionType, events, abortController)
generateSuggestions(projectPath, suggestionType, events, abortController, settingsService)
.catch((error) => {
logError(error, 'Generate suggestions failed (background)');
events.emit('suggestions:event', {

View File

@@ -16,6 +16,8 @@ import {
import { ProviderFactory } from '../providers/provider-factory.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { PathNotAllowedError } from '@automaker/platform';
import type { SettingsService } from './settings-service.js';
import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js';
interface Message {
id: string;
@@ -57,11 +59,13 @@ export class AgentService {
private stateDir: string;
private metadataFile: string;
private events: EventEmitter;
private settingsService: SettingsService | null = null;
constructor(dataDir: string, events: EventEmitter) {
constructor(dataDir: string, events: EventEmitter, settingsService?: SettingsService) {
this.stateDir = path.join(dataDir, 'agent-sessions');
this.metadataFile = path.join(dataDir, 'sessions-metadata.json');
this.events = events;
this.settingsService = settingsService ?? null;
}
async initialize(): Promise<void> {
@@ -186,12 +190,23 @@ export class AgentService {
// Determine the effective working directory for context loading
const effectiveWorkDir = workingDirectory || session.workingDirectory;
// Load autoLoadClaudeMd setting (project setting takes precedence over global)
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
effectiveWorkDir,
this.settingsService,
'[AgentService]'
);
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
const contextResult = await loadContextFiles({
projectPath: effectiveWorkDir,
fsModule: secureFs as Parameters<typeof loadContextFiles>[0]['fsModule'],
});
// When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication
// (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md
const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd);
// Build combined system prompt with base prompt and context files
const baseSystemPrompt = this.getSystemPrompt();
const combinedSystemPrompt = contextFilesPrompt
@@ -205,6 +220,7 @@ export class AgentService {
sessionModel: session.model,
systemPrompt: combinedSystemPrompt,
abortController: session.abortController!,
autoLoadClaudeMd,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -224,11 +240,12 @@ export class AgentService {
prompt: '', // Will be set below based on images
model: effectiveModel,
cwd: effectiveWorkDir,
systemPrompt: combinedSystemPrompt,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: maxTurns,
allowedTools: allowedTools,
abortController: session.abortController!,
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
settingSources: sdkOptions.settingSources,
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
};

View File

@@ -25,8 +25,14 @@ import { promisify } from 'util';
import path from 'path';
import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import { createAutoModeOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import {
createAutoModeOptions,
createCustomOptions,
validateWorkingDirectory,
} from '../lib/sdk-options.js';
import { FeatureLoader } from './feature-loader.js';
import type { SettingsService } from './settings-service.js';
import { getAutoLoadClaudeMdSetting, filterClaudeMdFromContext } from '../lib/settings-helpers.js';
const execAsync = promisify(exec);
@@ -341,9 +347,11 @@ export class AutoModeService {
private autoLoopAbortController: AbortController | null = null;
private config: AutoModeConfig | null = null;
private pendingApprovals = new Map<string, PendingApproval>();
private settingsService: SettingsService | null = null;
constructor(events: EventEmitter) {
constructor(events: EventEmitter, settingsService?: SettingsService) {
this.events = events;
this.settingsService = settingsService ?? null;
}
/**
@@ -551,14 +559,25 @@ export class AutoModeService {
// Update feature status to in_progress
await this.updateFeatureStatus(projectPath, featureId, 'in_progress');
// Load autoLoadClaudeMd setting to determine context loading strategy
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
this.settingsService,
'[AutoMode]'
);
// Build the prompt - use continuation prompt if provided (for recovery after plan approval)
let prompt: string;
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
const contextResult = await loadContextFiles({
projectPath,
fsModule: secureFs as Parameters<typeof loadContextFiles>[0]['fsModule'],
});
// When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication
// (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md
const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd);
if (options?.continuationPrompt) {
// Continuation prompt is used when recovering from a plan approval
// The plan was already approved, so skip the planning phase
@@ -604,6 +623,7 @@ export class AutoModeService {
planningMode: feature.planningMode,
requirePlanApproval: feature.requirePlanApproval,
systemPrompt: contextFilesPrompt || undefined,
autoLoadClaudeMd,
}
);
@@ -746,12 +766,23 @@ export class AutoModeService {
// No previous context
}
// Load autoLoadClaudeMd setting to determine context loading strategy
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
this.settingsService,
'[AutoMode]'
);
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
const contextResult = await loadContextFiles({
projectPath,
fsModule: secureFs as Parameters<typeof loadContextFiles>[0]['fsModule'],
});
// When autoLoadClaudeMd is enabled, filter out CLAUDE.md to avoid duplication
// (SDK handles CLAUDE.md via settingSources), but keep other context files like CODE_QUALITY.md
const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd);
// Build complete prompt with feature info, previous context, and follow-up instructions
let fullPrompt = `## Follow-up on Feature Implementation
@@ -879,6 +910,7 @@ Address the follow-up instructions above. Review the previous work and make the
planningMode: 'skip', // Follow-ups don't require approval
previousContent: previousContext || undefined,
systemPrompt: contextFilesPrompt || undefined,
autoLoadClaudeMd,
}
);
@@ -1065,11 +1097,6 @@ Address the follow-up instructions above. Review the previous work and make the
* Analyze project to gather context
*/
async analyzeProject(projectPath: string): Promise<void> {
// Validate project path before proceeding
// This is called here because analyzeProject builds ExecuteOptions directly
// without using a factory function from sdk-options.ts
validateWorkingDirectory(projectPath);
const abortController = new AbortController();
const analysisFeatureId = `analysis-${Date.now()}`;
@@ -1097,13 +1124,31 @@ Format your response as a structured markdown document.`;
const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderForModel(analysisModel);
const options: ExecuteOptions = {
prompt,
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
this.settingsService,
'[AutoMode]'
);
// Use createCustomOptions for centralized SDK configuration with CLAUDE.md support
const sdkOptions = createCustomOptions({
cwd: projectPath,
model: analysisModel,
maxTurns: 5,
cwd: projectPath,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
autoLoadClaudeMd,
});
const options: ExecuteOptions = {
prompt,
model: sdkOptions.model ?? analysisModel,
cwd: sdkOptions.cwd ?? projectPath,
maxTurns: sdkOptions.maxTurns,
allowedTools: sdkOptions.allowedTools as string[],
abortController,
settingSources: sdkOptions.settingSources,
};
const stream = provider.executeQuery(options);
@@ -1708,6 +1753,7 @@ This helps parse your summary correctly in the output logs.`;
requirePlanApproval?: boolean;
previousContent?: string;
systemPrompt?: string;
autoLoadClaudeMd?: boolean;
}
): Promise<void> {
const finalProjectPath = options?.projectPath || projectPath;
@@ -1780,11 +1826,19 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
return;
}
// Load autoLoadClaudeMd setting (project setting takes precedence over global)
// Use provided value if available, otherwise load from settings
const autoLoadClaudeMd =
options?.autoLoadClaudeMd !== undefined
? options.autoLoadClaudeMd
: await getAutoLoadClaudeMdSetting(finalProjectPath, this.settingsService, '[AutoMode]');
// Build SDK options using centralized configuration for feature implementation
const sdkOptions = createAutoModeOptions({
cwd: workDir,
model: model,
abortController,
autoLoadClaudeMd,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -1823,7 +1877,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
cwd: workDir,
allowedTools: allowedTools,
abortController,
systemPrompt: options?.systemPrompt,
systemPrompt: sdkOptions.systemPrompt,
settingSources: sdkOptions.settingSources,
};
// Execute via provider

View File

@@ -1,15 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import {
FolderOpen,
Folder,
ChevronRight,
Home,
ArrowLeft,
HardDrive,
CornerDownLeft,
Clock,
X,
} from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { FolderOpen, Folder, ChevronRight, HardDrive, Clock, X } from 'lucide-react';
import {
Dialog,
DialogContent,
@@ -19,7 +9,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { PathInput } from '@/components/ui/path-input';
import { getJSON, setJSON } from '@/lib/storage';
import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config';
@@ -78,7 +68,6 @@ export function FileBrowserDialog({
initialPath,
}: FileBrowserDialogProps) {
const [currentPath, setCurrentPath] = useState<string>('');
const [pathInput, setPathInput] = useState<string>('');
const [parentPath, setParentPath] = useState<string | null>(null);
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
const [drives, setDrives] = useState<string[]>([]);
@@ -86,7 +75,6 @@ export function FileBrowserDialog({
const [error, setError] = useState('');
const [warning, setWarning] = useState('');
const [recentFolders, setRecentFolders] = useState<string[]>([]);
const pathInputRef = useRef<HTMLInputElement>(null);
// Load recent folders when dialog opens
useEffect(() => {
@@ -120,7 +108,6 @@ export function FileBrowserDialog({
if (result.success) {
setCurrentPath(result.currentPath);
setPathInput(result.currentPath);
setParentPath(result.parentPath);
setDirectories(result.directories);
setDrives(result.drives || []);
@@ -142,11 +129,10 @@ export function FileBrowserDialog({
[browseDirectory]
);
// Reset current path when dialog closes
// Reset state when dialog closes
useEffect(() => {
if (!open) {
setCurrentPath('');
setPathInput('');
setParentPath(null);
setDirectories([]);
setError('');
@@ -172,9 +158,6 @@ export function FileBrowserDialog({
const pathToUse = initialPath || defaultDir;
if (pathToUse) {
// Pre-fill the path input immediately
setPathInput(pathToUse);
// Then browse to that directory
browseDirectory(pathToUse);
} else {
// No default directory, browse home directory
@@ -183,7 +166,6 @@ export function FileBrowserDialog({
} catch {
// If config fetch fails, try initialPath or fall back to home directory
if (initialPath) {
setPathInput(initialPath);
browseDirectory(initialPath);
} else {
browseDirectory();
@@ -199,34 +181,21 @@ export function FileBrowserDialog({
browseDirectory(dir.path);
};
const handleGoToParent = () => {
if (parentPath) {
browseDirectory(parentPath);
}
};
const handleGoHome = () => {
const handleGoHome = useCallback(() => {
browseDirectory();
};
}, [browseDirectory]);
const handleNavigate = useCallback(
(path: string) => {
browseDirectory(path);
},
[browseDirectory]
);
const handleSelectDrive = (drivePath: string) => {
browseDirectory(drivePath);
};
const handleGoToPath = () => {
const trimmedPath = pathInput.trim();
if (trimmedPath) {
browseDirectory(trimmedPath);
}
};
const handlePathInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleGoToPath();
}
};
const handleSelect = useCallback(() => {
if (currentPath) {
addRecentFolder(currentPath);
@@ -275,31 +244,15 @@ export function FileBrowserDialog({
</DialogHeader>
<div className="flex flex-col gap-2 min-h-[350px] flex-1 overflow-hidden py-1">
{/* Direct path input */}
<div className="flex items-center gap-1.5">
<Input
ref={pathInputRef}
type="text"
placeholder="Paste or type a full path (e.g., /home/user/projects/myapp)"
value={pathInput}
onChange={(e) => setPathInput(e.target.value)}
onKeyDown={handlePathInputKeyDown}
className="flex-1 font-mono text-xs h-8"
data-testid="path-input"
disabled={loading}
/>
<Button
variant="secondary"
size="sm"
onClick={handleGoToPath}
disabled={loading || !pathInput.trim()}
data-testid="go-to-path-button"
className="h-8 px-2"
>
<CornerDownLeft className="w-3.5 h-3.5 mr-1" />
Go
</Button>
</div>
{/* Path navigation */}
<PathInput
currentPath={currentPath}
parentPath={parentPath}
loading={loading}
error={!!error}
onNavigate={handleNavigate}
onHome={handleGoHome}
/>
{/* Recent folders */}
{recentFolders.length > 0 && (
@@ -352,35 +305,8 @@ export function FileBrowserDialog({
</div>
)}
{/* Current path breadcrumb */}
<div className="flex items-center gap-1.5 p-2 rounded-md bg-sidebar-accent/10 border border-sidebar-border">
<Button
variant="ghost"
size="sm"
onClick={handleGoHome}
className="h-6 px-1.5"
disabled={loading}
>
<Home className="w-3.5 h-3.5" />
</Button>
{parentPath && (
<Button
variant="ghost"
size="sm"
onClick={handleGoToParent}
className="h-6 px-1.5"
disabled={loading}
>
<ArrowLeft className="w-3.5 h-3.5" />
</Button>
)}
<div className="flex-1 font-mono text-xs truncate text-muted-foreground">
{currentPath || 'Loading...'}
</div>
</div>
{/* Directory list */}
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-md">
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-md scrollbar-styled">
{loading && (
<div className="flex items-center justify-center h-full p-4">
<div className="text-xs text-muted-foreground">Loading directories...</div>
@@ -423,8 +349,8 @@ export function FileBrowserDialog({
</div>
<div className="text-[10px] text-muted-foreground">
Paste a full path above, or click on folders to navigate. Press Enter or click Go to
jump to a path.
Paste a full path above, or click on folders to navigate. Press Enter or click to jump
to a path.
</div>
</div>

View File

@@ -0,0 +1,102 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot="breadcrumb-item"
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'a';
return (
<Comp
data-slot="breadcrumb-link"
className={cn('hover:text-foreground transition-colors', className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn('text-foreground font-normal', className)}
{...props}
/>
);
}
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn('flex size-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -0,0 +1,296 @@
import { useEffect, Fragment, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
import { useState, useRef, useCallback, useMemo } from 'react';
import { Home, ArrowLeft, Pencil, ArrowRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { cn } from '@/lib/utils';
interface BreadcrumbSegment {
name: string;
path: string;
isLast: boolean;
}
function parseBreadcrumbs(path: string): BreadcrumbSegment[] {
if (!path) return [];
// Handle root path on Unix-like systems
if (path === '/') {
return [{ name: '/', path: '/', isLast: true }];
}
const segments = path.split(/[/\\]/).filter(Boolean);
const isWindows = segments[0]?.includes(':');
return segments.map((segment, index) => {
let fullPath: string;
if (isWindows) {
const pathParts = segments.slice(0, index + 1);
if (index === 0) {
fullPath = `${pathParts[0]}\\`;
} else {
fullPath = pathParts.join('\\');
}
} else {
fullPath = '/' + segments.slice(0, index + 1).join('/');
}
return {
name: segment,
path: fullPath,
isLast: index === segments.length - 1,
};
});
}
interface PathInputProps {
/** Current resolved path */
currentPath: string;
/** Parent path for back navigation (null if at root) */
parentPath: string | null;
/** Whether the component is in a loading state */
loading?: boolean;
/** Whether there's an error (shows input mode and red border when true) */
error?: boolean;
/** Placeholder text for the input field */
placeholder?: string;
/** Called when user navigates to a path (via breadcrumb click, enter key, or navigation buttons) */
onNavigate: (path: string) => void;
/** Called when user clicks home button (navigates to home directory) */
onHome: () => void;
/** Additional className for the container */
className?: string;
}
function PathInput({
currentPath,
parentPath,
loading = false,
error,
placeholder = 'Paste or type a full path (e.g., /home/user/projects/myapp)',
onNavigate,
onHome,
className,
}: PathInputProps) {
const [isEditing, setIsEditing] = useState(false);
const [pathInput, setPathInput] = useState(currentPath);
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Sync pathInput with currentPath when it changes externally
useEffect(() => {
if (!isEditing) {
setPathInput(currentPath);
}
}, [currentPath, isEditing]);
// Focus input when error occurs or entering edit mode
useEffect(() => {
if ((error || isEditing) && inputRef.current) {
inputRef.current.focus();
if (error) {
inputRef.current.select();
}
}
}, [error, isEditing]);
const handleGoToParent = useCallback(() => {
if (parentPath) {
onNavigate(parentPath);
}
}, [parentPath, onNavigate]);
const handleBreadcrumbClick = useCallback(
(path: string) => {
onNavigate(path);
},
[onNavigate]
);
const handleStartEditing = useCallback(() => {
setIsEditing(true);
}, []);
const handleInputBlur = useCallback(
(e: FocusEvent) => {
// Check if focus is moving to another element within this component
if (containerRef.current?.contains(e.relatedTarget)) {
return;
}
if (pathInput !== currentPath) {
setPathInput(currentPath);
}
setIsEditing(false);
},
[pathInput, currentPath]
);
const handleGoToPath = useCallback(() => {
const trimmedPath = pathInput.trim();
if (trimmedPath) {
onNavigate(trimmedPath);
setIsEditing(false);
}
}, [pathInput, onNavigate]);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleGoToPath();
} else if (e.key === 'Escape') {
e.preventDefault();
setPathInput(currentPath);
setIsEditing(false);
inputRef.current?.blur();
}
},
[handleGoToPath, currentPath]
);
// Handle click on the path container to start editing
const handleContainerClick = useCallback(
(e: MouseEvent) => {
// Don't trigger if clicking on a button or already editing
if (
isEditing ||
(e.target as HTMLElement).closest('button') ||
(e.target as HTMLElement).closest('a')
) {
return;
}
setIsEditing(true);
},
[isEditing]
);
const breadcrumbs = useMemo(() => parseBreadcrumbs(currentPath), [currentPath]);
const showBreadcrumbs = currentPath && !isEditing && !loading && !error;
return (
<div
ref={containerRef}
className={cn('flex items-center gap-2', className)}
role="navigation"
aria-label="Path navigation"
>
{/* Navigation buttons */}
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="icon"
onClick={onHome}
className="h-7 w-7"
disabled={loading}
aria-label="Go to home directory"
>
<Home className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleGoToParent}
className="h-7 w-7"
disabled={loading || !parentPath}
aria-label="Go to parent directory"
>
<ArrowLeft className="w-4 h-4" />
</Button>
</div>
{/* Path display / input */}
<div
onClick={handleContainerClick}
className={cn(
'flex-1 flex items-center gap-2 min-w-0 h-8 px-3 rounded-md border bg-background/50 transition-colors',
error
? 'border-destructive focus-within:border-destructive'
: 'border-input focus-within:border-ring focus-within:ring-1 focus-within:ring-ring',
!isEditing && !error && 'cursor-text hover:border-ring/50'
)}
>
{showBreadcrumbs ? (
<>
<Breadcrumb className="flex-1 min-w-0 overflow-hidden">
<BreadcrumbList className="flex-nowrap overflow-x-auto scrollbar-none">
{breadcrumbs.map((crumb) => (
<Fragment key={crumb.path}>
<BreadcrumbItem className="shrink-0">
{crumb.isLast ? (
<BreadcrumbPage className="font-mono text-xs font-medium truncate max-w-[200px]">
{crumb.name}
</BreadcrumbPage>
) : (
<BreadcrumbLink
href="#"
onClick={(e) => {
e.preventDefault();
handleBreadcrumbClick(crumb.path);
}}
className="font-mono text-xs text-muted-foreground hover:text-foreground transition-colors truncate max-w-[150px]"
>
{crumb.name}
</BreadcrumbLink>
)}
</BreadcrumbItem>
{!crumb.isLast && <BreadcrumbSeparator className="[&>svg]:size-3.5 shrink-0" />}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
<Button
variant="ghost"
size="icon"
onClick={handleStartEditing}
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
aria-label="Edit path"
>
<Pencil className="w-3.5 h-3.5" />
</Button>
</>
) : (
<>
<Input
ref={inputRef}
type="text"
placeholder={placeholder}
value={pathInput}
onChange={(e) => setPathInput(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className="flex-1 font-mono text-xs h-7 px-0 border-0 shadow-none focus-visible:ring-0 bg-transparent"
data-testid="path-input"
disabled={loading}
aria-label="Path input"
aria-invalid={error}
/>
<Button
variant="ghost"
size="icon"
onClick={handleGoToPath}
disabled={!pathInput.trim() || loading}
className="h-6 w-6 shrink-0"
aria-label="Go to path"
>
<ArrowRight className="w-3.5 h-3.5" />
</Button>
</>
)}
</div>
</div>
);
}
export { PathInput, parseBreadcrumbs };
export type { PathInputProps, BreadcrumbSegment };

View File

@@ -10,6 +10,7 @@ import { SettingsNavigation } from './settings-view/components/settings-navigati
import { ApiKeysSection } from './settings-view/api-keys/api-keys-section';
import { ClaudeUsageSection } from './settings-view/api-keys/claude-usage-section';
import { ClaudeCliStatus } from './settings-view/cli-status/claude-cli-status';
import { ClaudeMdSettings } from './settings-view/claude/claude-md-settings';
import { AIEnhancementSection } from './settings-view/ai-enhancement';
import { AppearanceSection } from './settings-view/appearance/appearance-section';
import { TerminalSection } from './settings-view/terminal/terminal-section';
@@ -47,6 +48,8 @@ export function SettingsView() {
apiKeys,
validationModel,
setValidationModel,
autoLoadClaudeMd,
setAutoLoadClaudeMd,
} = useAppStore();
// Hide usage tracking when using API key (only show for Claude Code CLI users)
@@ -102,6 +105,10 @@ export function SettingsView() {
isChecking={isCheckingClaudeCli}
onRefresh={handleRefreshClaudeCli}
/>
<ClaudeMdSettings
autoLoadClaudeMd={autoLoadClaudeMd}
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
/>
{showUsageTracking && <ClaudeUsageSection />}
</div>
);

View File

@@ -0,0 +1,82 @@
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { FileCode } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ClaudeMdSettingsProps {
autoLoadClaudeMd: boolean;
onAutoLoadClaudeMdChange: (enabled: boolean) => void;
}
/**
* ClaudeMdSettings Component
*
* UI control for the autoLoadClaudeMd setting which enables automatic loading
* of project instructions from .claude/CLAUDE.md files via the Claude Agent SDK.
*
* Usage:
* ```tsx
* <ClaudeMdSettings
* autoLoadClaudeMd={autoLoadClaudeMd}
* onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
* />
* ```
*/
export function ClaudeMdSettings({
autoLoadClaudeMd,
onAutoLoadClaudeMdChange,
}: ClaudeMdSettingsProps) {
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
data-testid="claude-md-settings"
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<FileCode className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
CLAUDE.md Integration
</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure automatic loading of project-specific instructions.
</p>
</div>
<div className="p-6">
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="auto-load-claude-md"
checked={autoLoadClaudeMd}
onCheckedChange={(checked) => onAutoLoadClaudeMdChange(checked === true)}
className="mt-1"
data-testid="auto-load-claude-md-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="auto-load-claude-md"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<FileCode className="w-4 h-4 text-brand-500" />
Auto-load CLAUDE.md Files
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Automatically load project instructions from{' '}
<code className="text-[10px] px-1 py-0.5 rounded bg-accent/50">
.claude/CLAUDE.md
</code>{' '}
files. When enabled, Claude will read and follow conventions specified in your
project&apos;s CLAUDE.md file. Project settings override global settings.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -223,6 +223,7 @@ export async function syncSettingsToServer(): Promise<boolean> {
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
validationModel: state.validationModel,
autoLoadClaudeMd: state.autoLoadClaudeMd,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles,
projects: state.projects,

View File

@@ -478,6 +478,9 @@ export interface AppState {
// Validation Model Settings
validationModel: AgentModel; // Model used for GitHub issue validation (default: opus)
// Claude Agent SDK Settings
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
// Project Analysis
projectAnalysis: ProjectAnalysis | null;
isAnalyzing: boolean;
@@ -751,6 +754,9 @@ export interface AppActions {
// Validation Model actions
setValidationModel: (model: AgentModel) => void;
// Claude Agent SDK Settings actions
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
@@ -922,6 +928,7 @@ const initialState: AppState = {
muteDoneSound: false, // Default to sound enabled (not muted)
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
validationModel: 'opus', // Default to opus for GitHub issue validation
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
isAnalyzing: false,
@@ -1547,6 +1554,14 @@ export const useAppStore = create<AppState & AppActions>()(
// Validation Model actions
setValidationModel: (model) => set({ validationModel: model }),
// Claude Agent SDK Settings actions
setAutoLoadClaudeMd: async (enabled) => {
set({ autoLoadClaudeMd: enabled });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
// AI Profile actions
addAIProfile: (profile) => {
const id = `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -2690,6 +2705,7 @@ export const useAppStore = create<AppState & AppActions>()(
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
validationModel: state.validationModel,
autoLoadClaudeMd: state.autoLoadClaudeMd,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,

View File

@@ -45,29 +45,45 @@ ${addedLines}
/**
* Generate a synthetic unified diff for an untracked (new) file
* This is needed because `git diff HEAD` doesn't include untracked files
*
* If the path is a directory, this will recursively generate diffs for all files inside
*/
export async function generateSyntheticDiffForNewFile(
basePath: string,
relativePath: string
): Promise<string> {
const fullPath = path.join(basePath, relativePath);
// Remove trailing slash if present (git status reports directories with trailing /)
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
const fullPath = path.join(basePath, cleanPath);
try {
// Check if it's a binary file
if (isBinaryFile(relativePath)) {
return `diff --git a/${relativePath} b/${relativePath}
new file mode 100644
index 0000000..0000000
Binary file ${relativePath} added
`;
}
// Get file stats to check size and type
const stats = await secureFs.stat(fullPath);
// Check if it's a directory (can happen with untracked directories from git status)
// Check if it's a directory first (before binary check)
// This handles edge cases like directories named "images.png/"
if (stats.isDirectory()) {
return createNewFileDiff(relativePath, '040000', ['[Directory]']);
const filesInDir = await listAllFilesInDirectory(basePath, cleanPath);
if (filesInDir.length === 0) {
// Empty directory
return createNewFileDiff(cleanPath, '040000', ['[Empty directory]']);
}
// Generate diffs for all files in the directory sequentially
// Using sequential processing to avoid exhausting file descriptors on large directories
const diffs: string[] = [];
for (const filePath of filesInDir) {
diffs.push(await generateSyntheticDiffForNewFile(basePath, filePath));
}
return diffs.join('');
}
// Check if it's a binary file (after directory check to handle dirs with binary extensions)
if (isBinaryFile(cleanPath)) {
return `diff --git a/${cleanPath} b/${cleanPath}
new file mode 100644
index 0000000..0000000
Binary file ${cleanPath} added
`;
}
const fileSize = Number(stats.size);
@@ -92,11 +108,11 @@ Binary file ${relativePath} added
const lineCount = lines.length;
const addedLines = lines.map((line) => `+${line}`).join('\n');
let diff = `diff --git a/${relativePath} b/${relativePath}
let diff = `diff --git a/${cleanPath} b/${cleanPath}
new file mode 100644
index 0000000..0000000
--- /dev/null
+++ b/${relativePath}
+++ b/${cleanPath}
@@ -0,0 +1,${lineCount} @@
${addedLines}`;
@@ -110,7 +126,7 @@ ${addedLines}`;
// Log the error for debugging
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
// Return a placeholder diff
return createNewFileDiff(relativePath, '100644', ['[Unable to read file content]']);
return createNewFileDiff(cleanPath, '100644', ['[Unable to read file content]']);
}
}

View File

@@ -118,7 +118,7 @@ describe('diff.ts', () => {
expect(diff).toContain('[Unable to read file content]');
});
it('should handle directory path gracefully', async () => {
it('should handle empty directory path gracefully', async () => {
const dirName = 'some-directory';
const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath);
@@ -127,7 +127,38 @@ describe('diff.ts', () => {
expect(diff).toContain(`diff --git a/${dirName} b/${dirName}`);
expect(diff).toContain('new file mode 040000');
expect(diff).toContain('[Directory]');
expect(diff).toContain('[Empty directory]');
});
it('should expand directory with files and generate diffs for each file', async () => {
const dirName = 'new-feature';
const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath);
await fs.writeFile(path.join(dirPath, 'index.ts'), 'export const foo = 1;\n');
await fs.writeFile(path.join(dirPath, 'utils.ts'), 'export const bar = 2;\n');
const diff = await generateSyntheticDiffForNewFile(tempDir, dirName);
// Should contain diffs for both files in the directory
expect(diff).toContain(`diff --git a/${dirName}/index.ts b/${dirName}/index.ts`);
expect(diff).toContain(`diff --git a/${dirName}/utils.ts b/${dirName}/utils.ts`);
expect(diff).toContain('+export const foo = 1;');
expect(diff).toContain('+export const bar = 2;');
// Should NOT contain a diff for the directory itself
expect(diff).not.toContain('[Empty directory]');
});
it('should handle directory path with trailing slash', async () => {
const dirName = 'trailing-slash-dir';
const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath);
await fs.writeFile(path.join(dirPath, 'file.txt'), 'content\n');
// git status reports untracked directories with trailing slash
const diff = await generateSyntheticDiffForNewFile(tempDir, `${dirName}/`);
expect(diff).toContain(`diff --git a/${dirName}/file.txt b/${dirName}/file.txt`);
expect(diff).toContain('+content');
});
});

View File

@@ -19,6 +19,15 @@ export interface ConversationMessage {
content: string | Array<{ type: string; text?: string; source?: object }>;
}
/**
* System prompt preset configuration for CLAUDE.md auto-loading
*/
export interface SystemPromptPreset {
type: 'preset';
preset: 'claude_code';
append?: string;
}
/**
* Options for executing a query via a provider
*/
@@ -26,13 +35,14 @@ export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string;
systemPrompt?: string | SystemPromptPreset;
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Sources for CLAUDE.md loading
}
/**

View File

@@ -297,6 +297,10 @@ export interface GlobalSettings {
// Window State (Electron only)
/** Persisted window bounds for restoring position/size across sessions */
windowBounds?: WindowBounds;
// Claude Agent SDK Settings
/** Auto-load CLAUDE.md files using SDK's settingSources option */
autoLoadClaudeMd?: boolean;
}
/**
@@ -392,6 +396,10 @@ export interface ProjectSettings {
// Session Tracking
/** Last chat session selected in this project */
lastSelectedSessionId?: string;
// Claude Agent SDK Settings
/** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */
autoLoadClaudeMd?: boolean;
}
/**
@@ -450,6 +458,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
recentFolders: [],
worktreePanelCollapsed: false,
lastSelectedSessionByProject: {},
autoLoadClaudeMd: false,
};
/** Default credentials (empty strings - user must provide API keys) */