diff --git a/auto-claude-ui/src/main/ipc-handlers/task-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/task-handlers.ts
index 8d134078..df4e18e4 100644
--- a/auto-claude-ui/src/main/ipc-handlers/task-handlers.ts
+++ b/auto-claude-ui/src/main/ipc-handlers/task-handlers.ts
@@ -260,7 +260,7 @@ export function registerTaskHandlers(
async (
_,
taskId: string,
- updates: { title?: string; description?: string }
+ updates: { title?: string; description?: string; metadata?: Partial }
): Promise> => {
try {
// Find task and project
@@ -363,11 +363,80 @@ export function registerTaskHandlers(
}
}
+ // Update metadata if provided
+ let updatedMetadata = task.metadata;
+ if (updates.metadata) {
+ updatedMetadata = { ...task.metadata, ...updates.metadata };
+
+ // Process and save attached images if provided
+ if (updates.metadata.attachedImages && updates.metadata.attachedImages.length > 0) {
+ const attachmentsDir = path.join(specDir, 'attachments');
+ mkdirSync(attachmentsDir, { recursive: true });
+
+ const savedImages: typeof updates.metadata.attachedImages = [];
+
+ for (const image of updates.metadata.attachedImages) {
+ // If image has data (new image), save it
+ if (image.data) {
+ try {
+ const buffer = Buffer.from(image.data, 'base64');
+ const imagePath = path.join(attachmentsDir, image.filename);
+ writeFileSync(imagePath, buffer);
+
+ savedImages.push({
+ id: image.id,
+ filename: image.filename,
+ mimeType: image.mimeType,
+ size: image.size,
+ path: `attachments/${image.filename}`
+ });
+ } catch (err) {
+ console.error(`Failed to save image ${image.filename}:`, err);
+ }
+ } else if (image.path) {
+ // Existing image, keep it
+ savedImages.push(image);
+ }
+ }
+
+ updatedMetadata.attachedImages = savedImages;
+ }
+
+ // Update task_metadata.json
+ const metadataPath = path.join(specDir, 'task_metadata.json');
+ try {
+ writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2));
+ } catch (err) {
+ console.error('Failed to update task_metadata.json:', err);
+ }
+
+ // Update requirements.json if it exists
+ const requirementsPath = path.join(specDir, 'requirements.json');
+ if (existsSync(requirementsPath)) {
+ try {
+ const requirementsContent = readFileSync(requirementsPath, 'utf-8');
+ const requirements = JSON.parse(requirementsContent);
+
+ if (updates.description !== undefined) {
+ requirements.task_description = updates.description;
+ }
+ if (updates.metadata.category) {
+ requirements.workflow_type = updates.metadata.category;
+ }
+
+ writeFileSync(requirementsPath, JSON.stringify(requirements, null, 2));
+ } catch (err) {
+ console.error('Failed to update requirements.json:', err);
+ }
+ }
+ }
+
// Build the updated task object
const updatedTask: Task = {
...task,
title: finalTitle ?? task.title,
description: updates.description ?? task.description,
+ metadata: updatedMetadata,
updatedAt: new Date()
};
diff --git a/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts
index 5b00ae7d..ed9350d1 100644
--- a/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts
+++ b/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts
@@ -5,6 +5,7 @@ import type { IPCResult, TerminalCreateOptions, ClaudeProfile, ClaudeProfileSett
import { getClaudeProfileManager } from '../claude-profile-manager';
import { TerminalManager } from '../terminal-manager';
import { projectStore } from '../project-store';
+import { terminalNameGenerator } from '../terminal-name-generator';
/**
@@ -53,6 +54,25 @@ export function registerTerminalHandlers(
}
);
+ ipcMain.handle(
+ IPC_CHANNELS.TERMINAL_GENERATE_NAME,
+ async (_, command: string, cwd?: string): Promise> => {
+ try {
+ const name = await terminalNameGenerator.generateName(command, cwd);
+ if (name) {
+ return { success: true, data: name };
+ } else {
+ return { success: false, error: 'Failed to generate terminal name' };
+ }
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to generate terminal name'
+ };
+ }
+ }
+ );
+
// Claude profile management (multi-account support)
ipcMain.handle(
IPC_CHANNELS.CLAUDE_PROFILES_GET,
diff --git a/auto-claude-ui/src/main/terminal-name-generator.ts b/auto-claude-ui/src/main/terminal-name-generator.ts
new file mode 100644
index 00000000..a9eb7999
--- /dev/null
+++ b/auto-claude-ui/src/main/terminal-name-generator.ts
@@ -0,0 +1,306 @@
+import path from 'path';
+import { existsSync, readFileSync } from 'fs';
+import { spawn } from 'child_process';
+import { app } from 'electron';
+import { EventEmitter } from 'events';
+import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from './rate-limit-detector';
+
+/**
+ * Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set
+ */
+const DEBUG = process.env.AUTO_CLAUDE_DEBUG === 'true' || process.env.AUTO_CLAUDE_DEBUG === '1';
+
+function debug(...args: unknown[]): void {
+ if (DEBUG) {
+ console.log('[TerminalNameGenerator]', ...args);
+ }
+}
+
+/**
+ * Service for generating terminal names from commands using Claude AI
+ */
+export class TerminalNameGenerator extends EventEmitter {
+ private pythonPath: string = 'python3';
+ private autoBuildSourcePath: string = '';
+
+ constructor() {
+ super();
+ debug('TerminalNameGenerator initialized');
+ }
+
+ /**
+ * Configure paths for Python and auto-claude source
+ */
+ configure(pythonPath?: string, autoBuildSourcePath?: string): void {
+ if (pythonPath) {
+ this.pythonPath = pythonPath;
+ }
+ if (autoBuildSourcePath) {
+ this.autoBuildSourcePath = autoBuildSourcePath;
+ }
+ }
+
+ /**
+ * Get the auto-claude source path (detects automatically if not configured)
+ */
+ private getAutoBuildSourcePath(): string | null {
+ if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
+ return this.autoBuildSourcePath;
+ }
+
+ const possiblePaths = [
+ path.resolve(__dirname, '..', '..', '..', 'auto-claude'),
+ path.resolve(app.getAppPath(), '..', 'auto-claude'),
+ path.resolve(process.cwd(), 'auto-claude')
+ ];
+
+ for (const p of possiblePaths) {
+ if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Load environment variables from auto-claude .env file
+ */
+ private loadAutoBuildEnv(): Record {
+ const autoBuildSource = this.getAutoBuildSourcePath();
+ if (!autoBuildSource) return {};
+
+ const envPath = path.join(autoBuildSource, '.env');
+ if (!existsSync(envPath)) return {};
+
+ try {
+ const envContent = readFileSync(envPath, 'utf-8');
+ const envVars: Record = {};
+
+ // Handle both Unix (\n) and Windows (\r\n) line endings
+ for (const line of envContent.split(/\r?\n/)) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) continue;
+
+ const eqIndex = trimmed.indexOf('=');
+ if (eqIndex > 0) {
+ const key = trimmed.substring(0, eqIndex).trim();
+ let value = trimmed.substring(eqIndex + 1).trim();
+
+ if ((value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))) {
+ value = value.slice(1, -1);
+ }
+
+ envVars[key] = value;
+ }
+ }
+
+ return envVars;
+ } catch {
+ return {};
+ }
+ }
+
+ /**
+ * Generate a terminal name from a command using Claude AI
+ * @param command - The command or recent output to generate a name from
+ * @param cwd - Current working directory for context
+ * @returns Promise resolving to the generated name (2-3 words) or null on failure
+ */
+ async generateName(command: string, cwd?: string): Promise {
+ const autoBuildSource = this.getAutoBuildSourcePath();
+
+ if (!autoBuildSource) {
+ debug('Auto-claude source path not found');
+ return null;
+ }
+
+ const prompt = this.createNamePrompt(command, cwd);
+ const script = this.createGenerationScript(prompt);
+
+ debug('Generating terminal name for command:', command.substring(0, 100) + '...');
+
+ const autoBuildEnv = this.loadAutoBuildEnv();
+ debug('Environment loaded', {
+ hasOAuthToken: !!autoBuildEnv.CLAUDE_CODE_OAUTH_TOKEN
+ });
+
+ // Get active Claude profile environment (CLAUDE_CONFIG_DIR if not default)
+ const profileEnv = getProfileEnv();
+
+ return new Promise((resolve) => {
+ const childProcess = spawn(this.pythonPath, ['-c', script], {
+ cwd: autoBuildSource,
+ env: {
+ ...process.env,
+ ...autoBuildEnv,
+ ...profileEnv, // Include active Claude profile config
+ PYTHONUNBUFFERED: '1'
+ }
+ });
+
+ let output = '';
+ let errorOutput = '';
+ const timeout = setTimeout(() => {
+ debug('Terminal name generation timed out after 30s');
+ childProcess.kill();
+ resolve(null);
+ }, 30000); // 30 second timeout
+
+ childProcess.stdout?.on('data', (data: Buffer) => {
+ output += data.toString();
+ });
+
+ childProcess.stderr?.on('data', (data: Buffer) => {
+ errorOutput += data.toString();
+ });
+
+ childProcess.on('exit', (code: number | null) => {
+ clearTimeout(timeout);
+
+ if (code === 0 && output.trim()) {
+ const name = this.cleanName(output.trim());
+ debug('Generated terminal name:', name);
+ resolve(name);
+ } else {
+ // Check for rate limit
+ const combinedOutput = `${output}\n${errorOutput}`;
+ const rateLimitDetection = detectRateLimit(combinedOutput);
+ if (rateLimitDetection.isRateLimited) {
+ debug('Rate limit detected:', {
+ resetTime: rateLimitDetection.resetTime,
+ limitType: rateLimitDetection.limitType,
+ suggestedProfile: rateLimitDetection.suggestedProfile?.name
+ });
+
+ const rateLimitInfo = createSDKRateLimitInfo('terminal-name-generator', rateLimitDetection);
+ this.emit('sdk-rate-limit', rateLimitInfo);
+ }
+
+ if (!rateLimitDetection.isRateLimited) {
+ debug('Terminal name generation failed', {
+ code,
+ errorOutput: errorOutput.substring(0, 500)
+ });
+ }
+ resolve(null);
+ }
+ });
+
+ childProcess.on('error', (err) => {
+ clearTimeout(timeout);
+ debug('Process error:', err.message);
+ resolve(null);
+ });
+ });
+ }
+
+ /**
+ * Create the prompt for terminal name generation
+ */
+ private createNamePrompt(command: string, cwd?: string): string {
+ let prompt = `Generate a very short, descriptive name (2-3 words MAX) for a terminal window based on what it's doing. The name should be concise and help identify the terminal at a glance.
+
+Command or activity:
+${command}`;
+
+ if (cwd) {
+ prompt += `
+
+Working directory:
+${cwd}`;
+ }
+
+ prompt += `
+
+Output ONLY the name (2-3 words), nothing else. Examples: "npm build", "git logs", "python tests", "claude dev"`;
+
+ return prompt;
+ }
+
+ /**
+ * Create the Python script to generate terminal name using Claude Agent SDK
+ */
+ private createGenerationScript(prompt: string): string {
+ // Escape the prompt for Python string - use JSON.stringify for safe escaping
+ const escapedPrompt = JSON.stringify(prompt);
+
+ return `
+import asyncio
+import sys
+
+async def generate_name():
+ try:
+ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
+
+ prompt = ${escapedPrompt}
+
+ # Create a minimal client for simple text generation (no tools needed)
+ client = ClaudeSDKClient(
+ options=ClaudeAgentOptions(
+ model="claude-haiku-4-5",
+ system_prompt="You generate very short, concise terminal names (2-3 words MAX). Output ONLY the name, nothing else. No quotes, no explanation, no preamble. Keep it as short as possible while being descriptive.",
+ max_turns=1,
+ )
+ )
+
+ async with client:
+ # Send the query
+ await client.query(prompt)
+
+ # Collect response text from AssistantMessage
+ response_text = ""
+ async for msg in client.receive_response():
+ msg_type = type(msg).__name__
+ if msg_type == "AssistantMessage" and hasattr(msg, "content"):
+ for block in msg.content:
+ block_type = type(block).__name__
+ if block_type == "TextBlock" and hasattr(block, "text"):
+ response_text += block.text
+
+ if response_text:
+ # Clean up the result
+ name = response_text.strip()
+ # Remove any quotes
+ name = name.strip('"').strip("'")
+ # Take first line only
+ name = name.split('\\n')[0].strip()
+ if name:
+ print(name)
+ sys.exit(0)
+
+ # If we get here, no valid response
+ sys.exit(1)
+
+ except ImportError as e:
+ print(f"Import error: {e}", file=sys.stderr)
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+asyncio.run(generate_name())
+`;
+ }
+
+ /**
+ * Clean up the generated name
+ */
+ private cleanName(name: string): string {
+ // Remove quotes if present
+ let cleaned = name.replace(/^["']|["']$/g, '');
+
+ // Remove any "Terminal:" or similar prefixes
+ cleaned = cleaned.replace(/^(terminal|name)[:\s]*/i, '');
+
+ // Truncate if too long (max 30 chars for terminal names)
+ if (cleaned.length > 30) {
+ cleaned = cleaned.substring(0, 27) + '...';
+ }
+
+ return cleaned.trim();
+ }
+}
+
+// Export singleton instance
+export const terminalNameGenerator = new TerminalNameGenerator();
diff --git a/auto-claude-ui/src/preload/api/terminal-api.ts b/auto-claude-ui/src/preload/api/terminal-api.ts
index c735c2a4..5564e19e 100644
--- a/auto-claude-ui/src/preload/api/terminal-api.ts
+++ b/auto-claude-ui/src/preload/api/terminal-api.ts
@@ -15,6 +15,7 @@ export interface TerminalAPI {
sendTerminalInput: (id: string, data: string) => void;
resizeTerminal: (id: string, cols: number, rows: number) => void;
invokeClaudeInTerminal: (id: string, cwd?: string) => void;
+ generateTerminalName: (command: string, cwd?: string) => Promise>;
// Terminal Session Management
getTerminalSessions: (projectPath: string) => Promise>;
@@ -81,6 +82,9 @@ export const createTerminalAPI = (): TerminalAPI => ({
invokeClaudeInTerminal: (id: string, cwd?: string): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_INVOKE_CLAUDE, id, cwd),
+ generateTerminalName: (command: string, cwd?: string): Promise> =>
+ ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GENERATE_NAME, command, cwd),
+
// Terminal Session Management
getTerminalSessions: (projectPath: string): Promise> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GET_SESSIONS, projectPath),
diff --git a/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx b/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx
index a7175780..857ffbb7 100644
--- a/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx
+++ b/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx
@@ -1,13 +1,16 @@
/**
- * TaskEditDialog - Dialog for editing task title and description
+ * TaskEditDialog - Dialog for editing task details
*
- * Allows users to modify the title and description of an existing task.
+ * Allows users to modify all task properties including title, description,
+ * classification fields, images, and review settings.
* Follows the same dialog pattern as TaskCreationWizard for consistency.
*
* Features:
* - Pre-populates form with current task values
- * - Form validation (title and description required)
- * - Shows attached images in read-only mode (if task has images)
+ * - Form validation (description required)
+ * - Editable classification fields (category, priority, complexity, impact)
+ * - Editable image attachments (add/remove images)
+ * - Editable review settings (requireReviewBeforeCoding)
* - Saves changes via persistUpdateTask (updates store + spec files)
* - Prevents save when no changes have been made
*
@@ -21,8 +24,8 @@
* />
* ```
*/
-import { useState, useEffect } from 'react';
-import { Loader2, Image as ImageIcon, ChevronDown, ChevronUp } from 'lucide-react';
+import { useState, useEffect, useCallback, useRef, type ClipboardEvent } from 'react';
+import { Loader2, Image as ImageIcon, ChevronDown, ChevronUp, X } from 'lucide-react';
import {
Dialog,
DialogContent,
@@ -35,9 +38,33 @@ import { Button } from './ui/button';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
+import { Checkbox } from './ui/checkbox';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from './ui/select';
+import {
+ ImageUpload,
+ generateImageId,
+ blobToBase64,
+ createThumbnail,
+ isValidImageMimeType,
+ resolveFilename
+} from './ImageUpload';
import { persistUpdateTask } from '../stores/task-store';
import { cn } from '../lib/utils';
-import type { Task, ImageAttachment } from '../../shared/types';
+import type { Task, ImageAttachment, TaskCategory, TaskPriority, TaskComplexity, TaskImpact } from '../../shared/types';
+import {
+ TASK_CATEGORY_LABELS,
+ TASK_PRIORITY_LABELS,
+ TASK_COMPLEXITY_LABELS,
+ TASK_IMPACT_LABELS,
+ MAX_IMAGES_PER_TASK,
+ ALLOWED_IMAGE_TYPES_DISPLAY
+} from '../../shared/constants';
/**
* Props for the TaskEditDialog component
@@ -58,21 +85,129 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
const [description, setDescription] = useState(task.description);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
+ const [showAdvanced, setShowAdvanced] = useState(false);
const [showImages, setShowImages] = useState(false);
+ const [pasteSuccess, setPasteSuccess] = useState(false);
- // Get attached images from task metadata
- const attachedImages: ImageAttachment[] = task.metadata?.attachedImages || [];
+ // Classification fields
+ const [category, setCategory] = useState(task.metadata?.category || '');
+ const [priority, setPriority] = useState(task.metadata?.priority || '');
+ const [complexity, setComplexity] = useState(task.metadata?.complexity || '');
+ const [impact, setImpact] = useState(task.metadata?.impact || '');
+
+ // Image attachments
+ const [images, setImages] = useState(task.metadata?.attachedImages || []);
+
+ // Review setting
+ const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(
+ task.metadata?.requireReviewBeforeCoding ?? false
+ );
+
+ // Ref for the textarea to handle paste events
+ const descriptionRef = useRef(null);
// Reset form when task changes or dialog opens
useEffect(() => {
if (open) {
setTitle(task.title);
setDescription(task.description);
+ setCategory(task.metadata?.category || '');
+ setPriority(task.metadata?.priority || '');
+ setComplexity(task.metadata?.complexity || '');
+ setImpact(task.metadata?.impact || '');
+ setImages(task.metadata?.attachedImages || []);
+ setRequireReviewBeforeCoding(task.metadata?.requireReviewBeforeCoding ?? false);
setError(null);
+
+ // Auto-expand sections if they have content
+ if (task.metadata?.category || task.metadata?.priority || task.metadata?.complexity || task.metadata?.impact) {
+ setShowAdvanced(true);
+ }
// Auto-expand images section if task has images
- setShowImages(attachedImages.length > 0);
+ setShowImages((task.metadata?.attachedImages || []).length > 0);
+ setPasteSuccess(false);
}
- }, [open, task.title, task.description, attachedImages.length]);
+ }, [open, task]);
+
+ /**
+ * Handle paste event for screenshot support
+ */
+ const handlePaste = useCallback(async (e: ClipboardEvent) => {
+ const clipboardItems = e.clipboardData?.items;
+ if (!clipboardItems) return;
+
+ // Find image items in clipboard
+ const imageItems: DataTransferItem[] = [];
+ for (let i = 0; i < clipboardItems.length; i++) {
+ const item = clipboardItems[i];
+ if (item.type.startsWith('image/')) {
+ imageItems.push(item);
+ }
+ }
+
+ // If no images, allow normal paste behavior
+ if (imageItems.length === 0) return;
+
+ // Prevent default paste when we have images
+ e.preventDefault();
+
+ // Check if we can add more images
+ const remainingSlots = MAX_IMAGES_PER_TASK - images.length;
+ if (remainingSlots <= 0) {
+ setError(`Maximum of ${MAX_IMAGES_PER_TASK} images allowed`);
+ return;
+ }
+
+ setError(null);
+
+ // Process image items
+ const newImages: ImageAttachment[] = [];
+ const existingFilenames = images.map(img => img.filename);
+
+ for (const item of imageItems.slice(0, remainingSlots)) {
+ const file = item.getAsFile();
+ if (!file) continue;
+
+ // Validate image type
+ if (!isValidImageMimeType(file.type)) {
+ setError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`);
+ continue;
+ }
+
+ try {
+ const dataUrl = await blobToBase64(file);
+ const thumbnail = await createThumbnail(dataUrl);
+
+ // Generate filename for pasted images (screenshot-timestamp.ext)
+ const extension = file.type.split('/')[1] || 'png';
+ const baseFilename = `screenshot-${Date.now()}.${extension}`;
+ const resolvedFilename = resolveFilename(baseFilename, [
+ ...existingFilenames,
+ ...newImages.map(img => img.filename)
+ ]);
+
+ newImages.push({
+ id: generateImageId(),
+ filename: resolvedFilename,
+ mimeType: file.type,
+ size: file.size,
+ data: dataUrl.split(',')[1], // Store base64 without data URL prefix
+ thumbnail
+ });
+ } catch {
+ setError('Failed to process pasted image');
+ }
+ }
+
+ if (newImages.length > 0) {
+ setImages(prev => [...prev, ...newImages]);
+ // Auto-expand images section
+ setShowImages(true);
+ // Show success feedback
+ setPasteSuccess(true);
+ setTimeout(() => setPasteSuccess(false), 2000);
+ }
+ }, [images]);
const handleSave = async () => {
// Validate input - only description is required
@@ -84,7 +219,17 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
// Check if anything changed
const trimmedTitle = title.trim();
const trimmedDescription = description.trim();
- if (trimmedTitle === task.title && trimmedDescription === task.description) {
+ const hasChanges =
+ trimmedTitle !== task.title ||
+ trimmedDescription !== task.description ||
+ category !== (task.metadata?.category || '') ||
+ priority !== (task.metadata?.priority || '') ||
+ complexity !== (task.metadata?.complexity || '') ||
+ impact !== (task.metadata?.impact || '') ||
+ requireReviewBeforeCoding !== (task.metadata?.requireReviewBeforeCoding ?? false) ||
+ JSON.stringify(images) !== JSON.stringify(task.metadata?.attachedImages || []);
+
+ if (!hasChanges) {
// No changes, just close
onOpenChange(false);
return;
@@ -93,10 +238,20 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
setIsSaving(true);
setError(null);
+ // Build metadata updates
+ const metadataUpdates: Partial = {};
+ if (category) metadataUpdates.category = category;
+ if (priority) metadataUpdates.priority = priority;
+ if (complexity) metadataUpdates.complexity = complexity;
+ if (impact) metadataUpdates.impact = impact;
+ if (images.length > 0) metadataUpdates.attachedImages = images;
+ metadataUpdates.requireReviewBeforeCoding = requireReviewBeforeCoding;
+
// Title is optional - if empty, it will be auto-generated by the backend
const success = await persistUpdateTask(task.id, {
title: trimmedTitle,
- description: trimmedDescription
+ description: trimmedDescription,
+ metadata: metadataUpdates
});
if (success) {
@@ -124,7 +279,7 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
Edit Task
- Update the task title and description. Changes will be saved to the spec files.
+ Update task details including title, description, classification, images, and settings. Changes will be saved to the spec files.
@@ -135,13 +290,18 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
Description *
- {/* Read-only Images Section (only shown if task has images) */}
- {attachedImages.length > 0 && (
- <>
- setShowImages(!showImages)}
- className={cn(
- 'flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors',
- 'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
- )}
- disabled={isSaving}
- >
-
-
- Attached Images
-
- {attachedImages.length}
-
-
- {showImages ? (
-
- ) : (
-
- )}
-
-
- {showImages && (
-
-
- These images were attached when the task was created. They cannot be modified.
-
-
- {attachedImages.map((image) => (
-
- {image.thumbnail ? (
-
- ) : (
-
-
-
- )}
-
-
- ))}
-
-
- )}
- >
+ {/* Paste Success Indicator */}
+ {pasteSuccess && (
+
+
+ Image added successfully!
+
)}
+ {/* Advanced Options Toggle */}
+ setShowAdvanced(!showAdvanced)}
+ className={cn(
+ 'flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors',
+ 'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
+ )}
+ disabled={isSaving}
+ >
+ Classification (optional)
+ {showAdvanced ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Advanced Options */}
+ {showAdvanced && (
+
+
+ {/* Category */}
+
+
+ Category
+
+ setCategory(value as TaskCategory)}
+ disabled={isSaving}
+ >
+
+
+
+
+ {Object.entries(TASK_CATEGORY_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ {/* Priority */}
+
+
+ Priority
+
+ setPriority(value as TaskPriority)}
+ disabled={isSaving}
+ >
+
+
+
+
+ {Object.entries(TASK_PRIORITY_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ {/* Complexity */}
+
+
+ Complexity
+
+ setComplexity(value as TaskComplexity)}
+ disabled={isSaving}
+ >
+
+
+
+
+ {Object.entries(TASK_COMPLEXITY_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ {/* Impact */}
+
+
+ Impact
+
+ setImpact(value as TaskImpact)}
+ disabled={isSaving}
+ >
+
+
+
+
+ {Object.entries(TASK_IMPACT_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+
+ These labels help organize and prioritize tasks. They're optional but useful for filtering.
+
+
+ )}
+
+ {/* Images Toggle */}
+ setShowImages(!showImages)}
+ className={cn(
+ 'flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors',
+ 'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
+ )}
+ disabled={isSaving}
+ >
+
+
+ Reference Images (optional)
+ {images.length > 0 && (
+
+ {images.length}
+
+ )}
+
+ {showImages ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Image Upload Section */}
+ {showImages && (
+
+
+ Attach screenshots, mockups, or diagrams to provide visual context for the AI.
+
+
+
+ )}
+
+ {/* Review Requirement Toggle */}
+
+
setRequireReviewBeforeCoding(checked === true)}
+ disabled={isSaving}
+ className="mt-0.5"
+ />
+
+
+ Require human review before coding
+
+
+ When enabled, you'll be prompted to review the spec and implementation plan before the coding phase begins. This allows you to approve, request changes, or provide feedback.
+
+
+
+
{/* Error */}
{error && (
-
- {error}
+
+
+ {error}
)}
diff --git a/auto-claude-ui/src/renderer/components/Terminal.tsx b/auto-claude-ui/src/renderer/components/Terminal.tsx
index 410febfa..20bc40d3 100644
--- a/auto-claude-ui/src/renderer/components/Terminal.tsx
+++ b/auto-claude-ui/src/renderer/components/Terminal.tsx
@@ -8,6 +8,7 @@ import { X, Sparkles, TerminalSquare, ListTodo, FileDown, ChevronDown, Circle, L
import { Button } from './ui/button';
import { cn } from '../lib/utils';
import { useTerminalStore, type TerminalStatus } from '../stores/terminal-store';
+import { useSettingsStore } from '../stores/settings-store';
import type { Task, ExecutionPhase } from '../../shared/types';
import {
Select,
@@ -67,6 +68,9 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
const isCreatedRef = useRef(false);
const isMountedRef = useRef(true);
const titleInputRef = useRef(null);
+ const commandBufferRef = useRef('');
+ const lastCommandRef = useRef('');
+ const autoNameTimeoutRef = useRef(null);
// Title editing state
const [isEditingTitle, setIsEditingTitle] = useState(false);
@@ -77,6 +81,7 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
const setClaudeMode = useTerminalStore((state) => state.setClaudeMode);
const updateTerminal = useTerminalStore((state) => state.updateTerminal);
const setAssociatedTask = useTerminalStore((state) => state.setAssociatedTask);
+ const autoNameTerminals = useSettingsStore((state) => state.settings.autoNameTerminals);
// Filter tasks to only show backlog (Planning) status tasks for dropdown
const backlogTasks = tasks.filter((t) => t.status === 'backlog');
@@ -92,6 +97,30 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
data: { type: 'terminal', terminalId: id }
});
+ // Auto-naming function - generates terminal name based on commands
+ const triggerAutoNaming = useCallback(async () => {
+ // Only auto-name if setting is enabled, not in Claude mode, and we have a command
+ if (!autoNameTerminals || terminal?.isClaudeMode || !lastCommandRef.current.trim()) {
+ return;
+ }
+
+ const command = lastCommandRef.current.trim();
+ // Skip very short or common commands
+ if (command.length < 2 || ['ls', 'cd', 'll', 'pwd', 'exit', 'clear'].includes(command)) {
+ return;
+ }
+
+ try {
+ const result = await window.electronAPI.generateTerminalName(command, terminal?.cwd || cwd);
+ if (result.success && result.data) {
+ updateTerminal(id, { title: result.data });
+ }
+ } catch (error) {
+ // Silently fail - auto-naming is not critical
+ console.debug('[Terminal] Auto-naming failed:', error);
+ }
+ }, [autoNameTerminals, terminal?.isClaudeMode, terminal?.cwd, cwd, id, updateTerminal]);
+
// Initialize xterm.js UI (separate from PTY creation)
useEffect(() => {
if (!terminalRef.current || xtermRef.current) return;
@@ -167,6 +196,32 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
// Handle terminal input - send to main process
xterm.onData((data) => {
window.electronAPI.sendTerminalInput(id, data);
+
+ // Track commands for auto-naming
+ if (data === '\r' || data === '\n') {
+ // Enter pressed - save the command and schedule auto-naming
+ lastCommandRef.current = commandBufferRef.current;
+ commandBufferRef.current = '';
+
+ // Clear any pending auto-naming
+ if (autoNameTimeoutRef.current) {
+ clearTimeout(autoNameTimeoutRef.current);
+ }
+
+ // Trigger auto-naming after a delay (wait for command to execute and produce output)
+ autoNameTimeoutRef.current = setTimeout(() => {
+ triggerAutoNaming();
+ }, 1500); // 1.5 second delay
+ } else if (data === '\x7f' || data === '\b') {
+ // Backspace - remove last char from buffer
+ commandBufferRef.current = commandBufferRef.current.slice(0, -1);
+ } else if (data === '\x03') {
+ // Ctrl+C - clear buffer
+ commandBufferRef.current = '';
+ } else if (data.charCodeAt(0) >= 32 && data.charCodeAt(0) < 127) {
+ // Printable character - add to buffer
+ commandBufferRef.current += data;
+ }
});
// Handle resize
@@ -346,6 +401,12 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
return () => {
isMountedRef.current = false;
+ // Clear auto-naming timeout
+ if (autoNameTimeoutRef.current) {
+ clearTimeout(autoNameTimeoutRef.current);
+ autoNameTimeoutRef.current = null;
+ }
+
// Delay cleanup to skip StrictMode's immediate remount
setTimeout(() => {
if (!isMountedRef.current) {
diff --git a/auto-claude-ui/src/renderer/components/TerminalGrid.tsx b/auto-claude-ui/src/renderer/components/TerminalGrid.tsx
index 6e5320de..6ab0c46a 100644
--- a/auto-claude-ui/src/renderer/components/TerminalGrid.tsx
+++ b/auto-claude-ui/src/renderer/components/TerminalGrid.tsx
@@ -80,13 +80,28 @@ export function TerminalGrid({ projectPath, onNewTaskClick }: TerminalGridProps)
fetchSessionDates();
}, [projectPath]);
+ // Get addRestoredTerminal from store
+ const addRestoredTerminal = useTerminalStore((state) => state.addRestoredTerminal);
+
// Handle restoring sessions from a specific date
const handleRestoreFromDate = useCallback(async (date: string) => {
if (!projectPath || isRestoring) return;
setIsRestoring(true);
try {
- // First close all existing terminals
+ // First get the session data for this date (we need it after restore)
+ const sessionsResult = await window.electronAPI.getTerminalSessionsForDate(date, projectPath);
+ const sessionsToRestore = sessionsResult.success ? sessionsResult.data || [] : [];
+
+ console.log(`[TerminalGrid] Found ${sessionsToRestore.length} sessions to restore from ${date}`);
+
+ if (sessionsToRestore.length === 0) {
+ console.log('[TerminalGrid] No sessions found for this date');
+ setIsRestoring(false);
+ return;
+ }
+
+ // Close all existing terminals
for (const terminal of terminals) {
await window.electronAPI.destroyTerminal(terminal.id);
removeTerminal(terminal.id);
@@ -95,7 +110,7 @@ export function TerminalGrid({ projectPath, onNewTaskClick }: TerminalGridProps)
// Small delay to ensure cleanup
await new Promise(resolve => setTimeout(resolve, 100));
- // Restore sessions from the selected date
+ // Restore sessions from the selected date (creates PTYs in main process)
const result = await window.electronAPI.restoreTerminalSessionsFromDate(
date,
projectPath,
@@ -104,9 +119,20 @@ export function TerminalGrid({ projectPath, onNewTaskClick }: TerminalGridProps)
);
if (result.success && result.data) {
- console.log(`Restored ${result.data.restored} sessions from ${date}`);
- // The terminal-store's restoreTerminalSessions will be called by App.tsx
- // Or we can manually add the restored terminals
+ console.log(`[TerminalGrid] Main process restored ${result.data.restored} sessions from ${date}`);
+
+ // Add each successfully restored session to the renderer's terminal store
+ for (const sessionResult of result.data.sessions) {
+ if (sessionResult.success) {
+ // Find the full session data
+ const fullSession = sessionsToRestore.find(s => s.id === sessionResult.id);
+ if (fullSession) {
+ console.log(`[TerminalGrid] Adding restored terminal to store: ${fullSession.id}`);
+ addRestoredTerminal(fullSession);
+ }
+ }
+ }
+
// Refresh session dates to update counts
const datesResult = await window.electronAPI.getTerminalSessionDates(projectPath);
if (datesResult.success && datesResult.data) {
@@ -118,7 +144,7 @@ export function TerminalGrid({ projectPath, onNewTaskClick }: TerminalGridProps)
} finally {
setIsRestoring(false);
}
- }, [projectPath, terminals, removeTerminal, isRestoring]);
+ }, [projectPath, terminals, removeTerminal, addRestoredTerminal, isRestoring]);
// Setup drag sensors
const sensors = useSensors(
diff --git a/auto-claude-ui/src/renderer/components/settings/GeneralSettings.tsx b/auto-claude-ui/src/renderer/components/settings/GeneralSettings.tsx
index ea0804e9..1f49c2f3 100644
--- a/auto-claude-ui/src/renderer/components/settings/GeneralSettings.tsx
+++ b/auto-claude-ui/src/renderer/components/settings/GeneralSettings.tsx
@@ -1,6 +1,7 @@
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
+import { Switch } from '../ui/switch';
import { SettingsSection } from './SettingsSection';
import { AVAILABLE_MODELS } from '../../../shared/constants';
import type { AppSettings } from '../../../shared/types';
@@ -56,6 +57,23 @@ export function GeneralSettings({ settings, onSettingsChange, section }: General
+
+
+
+
+ AI Terminal Naming
+
+
+ Automatically name terminals based on commands (uses Haiku)
+
+
+
onSettingsChange({ ...settings, autoNameTerminals: checked })}
+ />
+
+
);
diff --git a/auto-claude-ui/src/renderer/components/settings/ProjectSelector.tsx b/auto-claude-ui/src/renderer/components/settings/ProjectSelector.tsx
index f82f0573..647be1c7 100644
--- a/auto-claude-ui/src/renderer/components/settings/ProjectSelector.tsx
+++ b/auto-claude-ui/src/renderer/components/settings/ProjectSelector.tsx
@@ -49,21 +49,23 @@ export function ProjectSelector({
return (
<>
-
+
-
+
-
+
+ {selectedProject?.name || 'Select a project...'}
+
-
@@ -73,18 +75,18 @@ export function ProjectSelector({
) : (
projects.map((project) => (
-
-
-
+
+
{project.name}
{
e.stopPropagation();
}}
@@ -93,7 +95,7 @@ export function ProjectSelector({
}}
onClick={(e) => handleRemoveProject(project.id, e)}
>
-
+
@@ -111,8 +113,8 @@ export function ProjectSelector({
{/* Project path - shown when project is selected */}
{selectedProject && (
-
{selectedProject.path}
diff --git a/auto-claude-ui/src/renderer/stores/task-store.ts b/auto-claude-ui/src/renderer/stores/task-store.ts
index bebc3dd2..46a2a050 100644
--- a/auto-claude-ui/src/renderer/stores/task-store.ts
+++ b/auto-claude-ui/src/renderer/stores/task-store.ts
@@ -272,11 +272,11 @@ export async function persistTaskStatus(
}
/**
- * Update task title/description and persist to file
+ * Update task title/description/metadata and persist to file
*/
export async function persistUpdateTask(
taskId: string,
- updates: { title?: string; description?: string }
+ updates: { title?: string; description?: string; metadata?: Partial }
): Promise {
const store = useTaskStore.getState();
@@ -289,6 +289,7 @@ export async function persistUpdateTask(
store.updateTask(taskId, {
title: result.data.title,
description: result.data.description,
+ metadata: result.data.metadata,
updatedAt: new Date()
});
return true;
diff --git a/auto-claude-ui/src/renderer/styles/globals.css b/auto-claude-ui/src/renderer/styles/globals.css
index 505f365d..9ae11230 100644
--- a/auto-claude-ui/src/renderer/styles/globals.css
+++ b/auto-claude-ui/src/renderer/styles/globals.css
@@ -37,6 +37,9 @@
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+
/* Semantic colors */
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
@@ -115,6 +118,9 @@
--sidebar: #FFFFFF;
--sidebar-foreground: #0B0B0F;
+ --popover: #FFFFFF;
+ --popover-foreground: #0B0B0F;
+
/* Semantic colors */
--success: #4EBE96;
--success-foreground: #FFFFFF;
@@ -173,6 +179,9 @@
--sidebar: #0E0E12;
--sidebar-foreground: #E6E6E6;
+ --popover: #1A1A1F;
+ --popover-foreground: #E6E6E6;
+
/* Semantic colors - muted for dark mode */
--success: #4EBE96;
--success-foreground: #0B0B0F;
diff --git a/auto-claude-ui/src/shared/constants.ts b/auto-claude-ui/src/shared/constants.ts
index ba6e1f54..88a82e95 100644
--- a/auto-claude-ui/src/shared/constants.ts
+++ b/auto-claude-ui/src/shared/constants.ts
@@ -89,6 +89,7 @@ export const DEFAULT_APP_SETTINGS = {
pythonPath: undefined as string | undefined,
autoBuildPath: undefined as string | undefined,
autoUpdateAutoBuild: true,
+ autoNameTerminals: true,
notifications: {
onTaskComplete: true,
onTaskFailed: true,
@@ -171,6 +172,7 @@ export const IPC_CHANNELS = {
TERMINAL_INPUT: 'terminal:input',
TERMINAL_RESIZE: 'terminal:resize',
TERMINAL_INVOKE_CLAUDE: 'terminal:invokeClaude',
+ TERMINAL_GENERATE_NAME: 'terminal:generateName',
// Terminal session management
TERMINAL_GET_SESSIONS: 'terminal:getSessions',
diff --git a/auto-claude-ui/src/shared/types/settings.ts b/auto-claude-ui/src/shared/types/settings.ts
index 4ffdf7da..62dc4fbb 100644
--- a/auto-claude-ui/src/shared/types/settings.ts
+++ b/auto-claude-ui/src/shared/types/settings.ts
@@ -11,6 +11,7 @@ export interface AppSettings {
pythonPath?: string;
autoBuildPath?: string;
autoUpdateAutoBuild: boolean;
+ autoNameTerminals: boolean;
notifications: NotificationSettings;
// Global API keys (used as defaults for all projects)
globalClaudeOAuthToken?: string;