mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge branch 'AutoMaker-Org:main' into feat/claude-usage-clean
This commit is contained in:
@@ -4,14 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { ImageIcon, X, Loader2 } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
export interface FeatureImagePath {
|
||||
id: string;
|
||||
path: string; // Path to the temp file
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
}
|
||||
import { useAppStore, type FeatureImagePath } from "@/store/app-store";
|
||||
|
||||
// Map to store preview data by image ID (persisted across component re-mounts)
|
||||
export type ImagePreviewMap = Map<string, string>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { useEffect, useCallback, useRef } from "react";
|
||||
import React, { useEffect, useCallback, useRef } from "react";
|
||||
import { Button, buttonVariants } from "./button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useAppStore, Feature } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import type { AutoModeEvent } from "@/types/electron";
|
||||
import { pathsEqual } from "@/lib/utils";
|
||||
import { getBlockingDependencies } from "@/lib/dependency-resolver";
|
||||
import { getBlockingDependencies } from "@automaker/dependency-resolver";
|
||||
import { BoardBackgroundModal } from "@/components/dialogs/board-background-modal";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useAutoMode } from "@/hooks/use-auto-mode";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { AlertCircle, Lock, Hand, Sparkles } from "lucide-react";
|
||||
import { getBlockingDependencies } from "@/lib/dependency-resolver";
|
||||
import { getBlockingDependencies } from "@automaker/dependency-resolver";
|
||||
|
||||
interface CardBadgeProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getElectronAPI } from "@/lib/electron";
|
||||
import { toast } from "sonner";
|
||||
import { useAutoMode } from "@/hooks/use-auto-mode";
|
||||
import { truncateDescription } from "@/lib/utils";
|
||||
import { getBlockingDependencies } from "@/lib/dependency-resolver";
|
||||
import { getBlockingDependencies } from "@automaker/dependency-resolver";
|
||||
|
||||
interface UseBoardActionsProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { Feature, useAppStore } from "@/store/app-store";
|
||||
import { resolveDependencies, getBlockingDependencies } from "@/lib/dependency-resolver";
|
||||
import { resolveDependencies, getBlockingDependencies } from "@automaker/dependency-resolver";
|
||||
|
||||
type ColumnId = Feature["status"];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AgentModel, ThinkingLevel } from "@/store/app-store";
|
||||
import type { AgentModel, ThinkingLevel } from "@/store/app-store";
|
||||
import {
|
||||
Brain,
|
||||
Zap,
|
||||
|
||||
@@ -6,48 +6,12 @@
|
||||
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Claude model aliases for convenience
|
||||
*/
|
||||
export const CLAUDE_MODEL_MAP: Record<string, string> = {
|
||||
haiku: "claude-haiku-4-5",
|
||||
sonnet: "claude-sonnet-4-20250514",
|
||||
opus: "claude-opus-4-5-20251101",
|
||||
} as const;
|
||||
// Import shared model constants and types
|
||||
import { CLAUDE_MODEL_MAP, DEFAULT_MODELS } from "@automaker/types";
|
||||
import { resolveModelString } from "@automaker/model-resolver";
|
||||
|
||||
/**
|
||||
* Default models per use case
|
||||
*/
|
||||
export const DEFAULT_MODELS = {
|
||||
chat: "claude-opus-4-5-20251101",
|
||||
default: "claude-opus-4-5-20251101",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Resolve a model alias to a full model string
|
||||
*/
|
||||
export function resolveModelString(
|
||||
modelKey?: string,
|
||||
defaultModel: string = DEFAULT_MODELS.default
|
||||
): string {
|
||||
if (!modelKey) {
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
// Full Claude model string - pass through
|
||||
if (modelKey.includes("claude-")) {
|
||||
return modelKey;
|
||||
}
|
||||
|
||||
// Check alias map
|
||||
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Unknown key - use default
|
||||
return defaultModel;
|
||||
}
|
||||
// Re-export for backward compatibility
|
||||
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, resolveModelString };
|
||||
|
||||
/**
|
||||
* Get the model for chat operations
|
||||
@@ -64,13 +28,13 @@ export function getChatModel(explicitModel?: string): string {
|
||||
}
|
||||
|
||||
const envModel =
|
||||
process.env.AUTOMAKER_MODEL_CHAT || process.env.AUTOMAKER_MODEL_DEFAULT;
|
||||
import.meta.env.AUTOMAKER_MODEL_CHAT || import.meta.env.AUTOMAKER_MODEL_DEFAULT;
|
||||
|
||||
if (envModel) {
|
||||
return resolveModelString(envModel);
|
||||
}
|
||||
|
||||
return DEFAULT_MODELS.chat;
|
||||
return DEFAULT_MODELS.claude;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,4 +55,3 @@ export const CHAT_TOOLS = [
|
||||
* Default max turns for chat
|
||||
*/
|
||||
export const CHAT_MAX_TURNS = 1000;
|
||||
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* Dependency Resolution Utility
|
||||
*
|
||||
* Provides topological sorting and dependency analysis for features.
|
||||
* Uses a modified Kahn's algorithm that respects both dependencies and priorities.
|
||||
*/
|
||||
|
||||
import type { Feature } from "@/store/app-store";
|
||||
|
||||
export interface DependencyResolutionResult {
|
||||
orderedFeatures: Feature[]; // Features in dependency-aware order
|
||||
circularDependencies: string[][]; // Groups of IDs forming cycles
|
||||
missingDependencies: Map<string, string[]>; // featureId -> missing dep IDs
|
||||
blockedFeatures: Map<string, string[]>; // featureId -> blocking dep IDs (incomplete dependencies)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves feature dependencies using topological sort with priority-aware ordering.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Build dependency graph and detect missing/blocked dependencies
|
||||
* 2. Apply Kahn's algorithm for topological sort
|
||||
* 3. Within same dependency level, sort by priority (1=high, 2=medium, 3=low)
|
||||
* 4. Detect circular dependencies for features that can't be ordered
|
||||
*
|
||||
* @param features - Array of features to order
|
||||
* @returns Resolution result with ordered features and dependency metadata
|
||||
*/
|
||||
export function resolveDependencies(features: Feature[]): DependencyResolutionResult {
|
||||
const featureMap = new Map<string, Feature>(features.map(f => [f.id, f]));
|
||||
const inDegree = new Map<string, number>();
|
||||
const adjacencyList = new Map<string, string[]>(); // dependencyId -> [dependentIds]
|
||||
const missingDependencies = new Map<string, string[]>();
|
||||
const blockedFeatures = new Map<string, string[]>();
|
||||
|
||||
// Initialize graph structures
|
||||
for (const feature of features) {
|
||||
inDegree.set(feature.id, 0);
|
||||
adjacencyList.set(feature.id, []);
|
||||
}
|
||||
|
||||
// Build dependency graph and detect missing/blocked dependencies
|
||||
for (const feature of features) {
|
||||
const deps = feature.dependencies || [];
|
||||
for (const depId of deps) {
|
||||
if (!featureMap.has(depId)) {
|
||||
// Missing dependency - track it
|
||||
if (!missingDependencies.has(feature.id)) {
|
||||
missingDependencies.set(feature.id, []);
|
||||
}
|
||||
missingDependencies.get(feature.id)!.push(depId);
|
||||
} else {
|
||||
// Valid dependency - add edge to graph
|
||||
adjacencyList.get(depId)!.push(feature.id);
|
||||
inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1);
|
||||
|
||||
// Check if dependency is incomplete (blocking)
|
||||
const depFeature = featureMap.get(depId)!;
|
||||
if (depFeature.status !== 'completed' && depFeature.status !== 'verified') {
|
||||
if (!blockedFeatures.has(feature.id)) {
|
||||
blockedFeatures.set(feature.id, []);
|
||||
}
|
||||
blockedFeatures.get(feature.id)!.push(depId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm with priority-aware selection
|
||||
const queue: Feature[] = [];
|
||||
const orderedFeatures: Feature[] = [];
|
||||
|
||||
// Helper to sort features by priority (lower number = higher priority)
|
||||
const sortByPriority = (a: Feature, b: Feature) =>
|
||||
(a.priority ?? 2) - (b.priority ?? 2);
|
||||
|
||||
// Start with features that have no dependencies (in-degree 0)
|
||||
for (const [id, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(featureMap.get(id)!);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort initial queue by priority
|
||||
queue.sort(sortByPriority);
|
||||
|
||||
// Process features in topological order
|
||||
while (queue.length > 0) {
|
||||
// Take highest priority feature from queue
|
||||
const current = queue.shift()!;
|
||||
orderedFeatures.push(current);
|
||||
|
||||
// Process features that depend on this one
|
||||
for (const dependentId of adjacencyList.get(current.id) || []) {
|
||||
const currentDegree = inDegree.get(dependentId);
|
||||
if (currentDegree === undefined) {
|
||||
throw new Error(`In-degree not initialized for feature ${dependentId}`);
|
||||
}
|
||||
const newDegree = currentDegree - 1;
|
||||
inDegree.set(dependentId, newDegree);
|
||||
|
||||
if (newDegree === 0) {
|
||||
queue.push(featureMap.get(dependentId)!);
|
||||
// Re-sort queue to maintain priority order
|
||||
queue.sort(sortByPriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect circular dependencies (features not in output = part of cycle)
|
||||
const circularDependencies: string[][] = [];
|
||||
const processedIds = new Set(orderedFeatures.map(f => f.id));
|
||||
|
||||
if (orderedFeatures.length < features.length) {
|
||||
// Find cycles using DFS
|
||||
const remaining = features.filter(f => !processedIds.has(f.id));
|
||||
const cycles = detectCycles(remaining, featureMap);
|
||||
circularDependencies.push(...cycles);
|
||||
|
||||
// Add remaining features at end (part of cycles)
|
||||
orderedFeatures.push(...remaining);
|
||||
}
|
||||
|
||||
return {
|
||||
orderedFeatures,
|
||||
circularDependencies,
|
||||
missingDependencies,
|
||||
blockedFeatures
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects circular dependencies using depth-first search
|
||||
*
|
||||
* @param features - Features that couldn't be topologically sorted (potential cycles)
|
||||
* @param featureMap - Map of all features by ID
|
||||
* @returns Array of cycles, where each cycle is an array of feature IDs
|
||||
*/
|
||||
function detectCycles(
|
||||
features: Feature[],
|
||||
featureMap: Map<string, Feature>
|
||||
): string[][] {
|
||||
const cycles: string[][] = [];
|
||||
const visited = new Set<string>();
|
||||
const recursionStack = new Set<string>();
|
||||
const currentPath: string[] = [];
|
||||
|
||||
function dfs(featureId: string): boolean {
|
||||
visited.add(featureId);
|
||||
recursionStack.add(featureId);
|
||||
currentPath.push(featureId);
|
||||
|
||||
const feature = featureMap.get(featureId);
|
||||
if (feature) {
|
||||
for (const depId of feature.dependencies || []) {
|
||||
if (!visited.has(depId)) {
|
||||
if (dfs(depId)) return true;
|
||||
} else if (recursionStack.has(depId)) {
|
||||
// Found cycle - extract it
|
||||
const cycleStart = currentPath.indexOf(depId);
|
||||
cycles.push(currentPath.slice(cycleStart));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentPath.pop();
|
||||
recursionStack.delete(featureId);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
if (!visited.has(feature.id)) {
|
||||
dfs(feature.id);
|
||||
}
|
||||
}
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a feature's dependencies are satisfied (all complete or verified)
|
||||
*
|
||||
* @param feature - Feature to check
|
||||
* @param allFeatures - All features in the project
|
||||
* @returns true if all dependencies are satisfied, false otherwise
|
||||
*/
|
||||
export function areDependenciesSatisfied(
|
||||
feature: Feature,
|
||||
allFeatures: Feature[]
|
||||
): boolean {
|
||||
if (!feature.dependencies || feature.dependencies.length === 0) {
|
||||
return true; // No dependencies = always ready
|
||||
}
|
||||
|
||||
return feature.dependencies.every(depId => {
|
||||
const dep = allFeatures.find(f => f.id === depId);
|
||||
return dep && (dep.status === 'completed' || dep.status === 'verified');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the blocking dependencies for a feature (dependencies that are incomplete)
|
||||
*
|
||||
* @param feature - Feature to check
|
||||
* @param allFeatures - All features in the project
|
||||
* @returns Array of feature IDs that are blocking this feature
|
||||
*/
|
||||
export function getBlockingDependencies(
|
||||
feature: Feature,
|
||||
allFeatures: Feature[]
|
||||
): string[] {
|
||||
if (!feature.dependencies || feature.dependencies.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return feature.dependencies.filter(depId => {
|
||||
const dep = allFeatures.find(f => f.id === depId);
|
||||
return dep && dep.status !== 'completed' && dep.status !== 'verified';
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { Project, TrashedProject } from "@/lib/electron";
|
||||
import type {
|
||||
Feature as BaseFeature,
|
||||
FeatureImagePath,
|
||||
AgentModel,
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
AIProfile,
|
||||
} from '@automaker/types';
|
||||
|
||||
export type ViewMode =
|
||||
| "welcome"
|
||||
@@ -238,6 +247,10 @@ export interface ChatSession {
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export type { FeatureImagePath, AgentModel, PlanningMode, ThinkingLevel, ModelProvider, AIProfile };
|
||||
|
||||
// UI-specific: base64-encoded images (not in shared types)
|
||||
export interface FeatureImage {
|
||||
id: string;
|
||||
data: string; // base64 encoded
|
||||
@@ -246,68 +259,22 @@ export interface FeatureImage {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface FeatureImagePath {
|
||||
id: string;
|
||||
path: string; // Path to the temp file
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
}
|
||||
// Available models for feature execution (alias for consistency)
|
||||
export type ClaudeModel = AgentModel;
|
||||
|
||||
// Available models for feature execution
|
||||
export type ClaudeModel = "opus" | "sonnet" | "haiku";
|
||||
export type AgentModel = ClaudeModel;
|
||||
|
||||
// Model provider type
|
||||
export type ModelProvider = "claude";
|
||||
|
||||
// Thinking level (budget_tokens) options
|
||||
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
||||
|
||||
// Planning mode for feature specifications
|
||||
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
|
||||
// AI Provider Profile - user-defined presets for model configurations
|
||||
export interface AIProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
provider: ModelProvider;
|
||||
isBuiltIn: boolean; // Built-in profiles cannot be deleted
|
||||
icon?: string; // Optional icon name from lucide
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
title?: string;
|
||||
titleGenerating?: boolean;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
// UI-specific Feature extension with UI-only fields and stricter types
|
||||
export interface Feature extends Omit<BaseFeature, 'steps' | 'imagePaths' | 'status'> {
|
||||
steps: string[]; // Required in UI (not optional)
|
||||
status:
|
||||
| "backlog"
|
||||
| "in_progress"
|
||||
| "waiting_approval"
|
||||
| "verified"
|
||||
| "completed";
|
||||
images?: FeatureImage[];
|
||||
imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context
|
||||
startedAt?: string; // ISO timestamp for when the card moved to in_progress
|
||||
skipTests?: boolean; // When true, skip TDD approach and require manual verification
|
||||
summary?: string; // Summary of what was done/modified by the agent
|
||||
model?: AgentModel; // Model to use for this feature (defaults to opus)
|
||||
thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none)
|
||||
error?: string; // Error message if the agent errored during processing
|
||||
priority?: number; // Priority: 1 = high, 2 = medium, 3 = low
|
||||
dependencies?: string[]; // Array of feature IDs this feature depends on
|
||||
// Branch info - worktree path is derived at runtime from branchName
|
||||
branchName?: string; // Name of the feature branch (undefined = use current worktree)
|
||||
justFinishedAt?: string; // ISO timestamp when agent just finished and moved to waiting_approval (shows badge for 2 minutes)
|
||||
planningMode?: PlanningMode; // Planning mode for this feature
|
||||
planSpec?: PlanSpec; // Generated spec/plan data
|
||||
requirePlanApproval?: boolean; // Whether to pause and require manual approval before implementation
|
||||
prUrl?: string; // Pull request URL when a PR has been created for this feature
|
||||
images?: FeatureImage[]; // UI-specific base64 images
|
||||
imagePaths?: FeatureImagePath[]; // Stricter type than base (no string | union)
|
||||
justFinishedAt?: string; // UI-specific: ISO timestamp when agent just finished
|
||||
prUrl?: string; // UI-specific: Pull request URL
|
||||
}
|
||||
|
||||
// Parsed task from spec (for spec and full planning modes)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Session types for agent conversations
|
||||
*/
|
||||
|
||||
export interface AgentSession {
|
||||
id: string;
|
||||
name: string;
|
||||
projectPath: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messageCount: number;
|
||||
isArchived: boolean;
|
||||
isDirty?: boolean; // Indicates session has completed work that needs review
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface SessionListItem extends AgentSession {
|
||||
preview?: string; // Last message preview
|
||||
}
|
||||
|
||||
export interface CreateSessionParams {
|
||||
name: string;
|
||||
projectPath: string;
|
||||
workingDirectory?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSessionParams {
|
||||
id: string;
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user