Improve changelog feature, version tracking, markdown/preview, persistent styling options
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -58,7 +59,9 @@
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"uuid": "^13.0.0",
|
||||
"zustand": "^5.0.9"
|
||||
|
||||
Generated
+885
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ import type {
|
||||
GitTagInfo
|
||||
} from '../../shared/types';
|
||||
import { ChangelogGenerator } from './generator';
|
||||
import { VersionSuggester } from './version-suggester';
|
||||
import { parseExistingChangelog } from './parser';
|
||||
import {
|
||||
getBranches,
|
||||
@@ -38,6 +39,7 @@ export class ChangelogService extends EventEmitter {
|
||||
private cachedEnv: Record<string, string> | null = null;
|
||||
private debugEnabled: boolean | null = null;
|
||||
private generator: ChangelogGenerator | null = null;
|
||||
private versionSuggester: VersionSuggester | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -240,6 +242,32 @@ export class ChangelogService extends EventEmitter {
|
||||
return this.generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the version suggester instance
|
||||
*/
|
||||
private getVersionSuggester(): VersionSuggester {
|
||||
if (!this.versionSuggester) {
|
||||
const autoBuildSource = this.getAutoBuildSourcePath();
|
||||
if (!autoBuildSource) {
|
||||
throw new Error('Auto-build source path not found');
|
||||
}
|
||||
|
||||
// Verify claude CLI is available
|
||||
if (this.claudePath !== 'claude' && !existsSync(this.claudePath)) {
|
||||
throw new Error(`Claude CLI not found. Please ensure Claude Code is installed. Looked for: ${this.claudePath}`);
|
||||
}
|
||||
|
||||
this.versionSuggester = new VersionSuggester(
|
||||
this.pythonPath,
|
||||
this.claudePath,
|
||||
autoBuildSource,
|
||||
this.isDebugEnabled()
|
||||
);
|
||||
}
|
||||
|
||||
return this.versionSuggester;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Task Management
|
||||
// ============================================
|
||||
@@ -432,7 +460,7 @@ export class ChangelogService extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest next version based on task types
|
||||
* Suggest next version based on task types (rule-based)
|
||||
*/
|
||||
suggestVersion(specs: TaskSpecContent[], currentVersion?: string): string {
|
||||
// Default starting version
|
||||
@@ -445,7 +473,7 @@ export class ChangelogService extends EventEmitter {
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
let [major, minor, patch] = parts;
|
||||
const [major, minor, patch] = parts;
|
||||
|
||||
// Analyze specs for version increment decision
|
||||
let hasBreakingChanges = false;
|
||||
@@ -473,6 +501,46 @@ export class ChangelogService extends EventEmitter {
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest version using AI analysis of git commits
|
||||
*/
|
||||
async suggestVersionFromCommits(
|
||||
projectPath: string,
|
||||
commits: import('../../shared/types').GitCommit[],
|
||||
currentVersion?: string
|
||||
): Promise<{ version: string; reason: string }> {
|
||||
try {
|
||||
// Default starting version
|
||||
if (!currentVersion) {
|
||||
return { version: '1.0.0', reason: 'Initial version' };
|
||||
}
|
||||
|
||||
const parts = currentVersion.split('.').map(Number);
|
||||
if (parts.length !== 3 || parts.some(isNaN)) {
|
||||
return { version: '1.0.0', reason: 'Invalid current version, resetting to 1.0.0' };
|
||||
}
|
||||
|
||||
// Use AI to analyze commits and suggest version bump
|
||||
const suggester = this.getVersionSuggester();
|
||||
const suggestion = await suggester.suggestVersionBump(commits, currentVersion);
|
||||
|
||||
this.debug('AI version suggestion', suggestion);
|
||||
|
||||
return {
|
||||
version: suggestion.version,
|
||||
reason: suggestion.reason
|
||||
};
|
||||
} catch (error) {
|
||||
this.debug('Error in AI version suggestion, falling back to patch bump', error);
|
||||
// Fallback to patch bump if AI fails
|
||||
const [major, minor, patch] = (currentVersion || '1.0.0').split('.').map(Number);
|
||||
return {
|
||||
version: `${major}.${minor}.${patch + 1}`,
|
||||
reason: 'Patch version bump (AI analysis failed)'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@@ -31,7 +31,7 @@ const FORMAT_TEMPLATES = {
|
||||
**Bug Fixes:**
|
||||
- [List fixes]`,
|
||||
|
||||
'github-release': (version: string) => `## What's New in v${version}
|
||||
'github-release': (version: string, date: string) => `## ${version} - ${date}
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -223,9 +223,9 @@ export function buildGitPrompt(
|
||||
let formatSpecificInstructions = '';
|
||||
if (request.format === 'github-release') {
|
||||
formatSpecificInstructions = `
|
||||
For GitHub Release format, create TWO parts:
|
||||
For GitHub Release format, create TWO parts after the version header:
|
||||
|
||||
PART 1 - "What's New" (summarized changes):
|
||||
PART 1 - Categorized changes (summarized):
|
||||
- Use category sections: New Features, Improvements, Bug Fixes, Documentation, Other Changes
|
||||
- ONLY include sections that have actual changes - skip empty sections entirely
|
||||
- Add a blank line between each bullet point for cleaner formatting
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Architecture:
|
||||
* - changelog-service.ts: Main service facade (orchestrates all operations)
|
||||
* - generator.ts: AI-powered changelog generation
|
||||
* - version-suggester.ts: AI-powered version bump suggestions
|
||||
* - parser.ts: Changelog and spec parsing logic
|
||||
* - formatter.ts: Prompt building and formatting
|
||||
* - git-integration.ts: Git operations (branches, tags, commits)
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
export { ChangelogService, changelogService } from './changelog-service';
|
||||
export { ChangelogGenerator } from './generator';
|
||||
export { VersionSuggester } from './version-suggester';
|
||||
export * from './parser';
|
||||
export * from './formatter';
|
||||
export * from './git-integration';
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { GitCommit } from '../../shared/types';
|
||||
import { getProfileEnv } from '../rate-limit-detector';
|
||||
|
||||
interface VersionSuggestion {
|
||||
version: string;
|
||||
reason: string;
|
||||
bumpType: 'major' | 'minor' | 'patch';
|
||||
}
|
||||
|
||||
/**
|
||||
* AI-powered version bump suggester using Claude SDK with haiku model
|
||||
* Analyzes commits to intelligently suggest semantic version bumps
|
||||
*/
|
||||
export class VersionSuggester {
|
||||
private debugEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private pythonPath: string,
|
||||
private claudePath: string,
|
||||
private autoBuildSourcePath: string,
|
||||
debugEnabled: boolean
|
||||
) {
|
||||
this.debugEnabled = debugEnabled;
|
||||
}
|
||||
|
||||
private debug(...args: unknown[]): void {
|
||||
if (this.debugEnabled) {
|
||||
console.log('[VersionSuggester]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest version bump using AI analysis of commits
|
||||
*/
|
||||
async suggestVersionBump(
|
||||
commits: GitCommit[],
|
||||
currentVersion: string
|
||||
): Promise<VersionSuggestion> {
|
||||
this.debug('suggestVersionBump called', {
|
||||
commitCount: commits.length,
|
||||
currentVersion
|
||||
});
|
||||
|
||||
// Build prompt for Claude to analyze commits
|
||||
const prompt = this.buildPrompt(commits, currentVersion);
|
||||
const script = this.createAnalysisScript(prompt);
|
||||
|
||||
// Build environment
|
||||
const spawnEnv = this.buildSpawnEnvironment();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(this.pythonPath, ['-c', script], {
|
||||
cwd: this.autoBuildSourcePath,
|
||||
env: spawnEnv
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
childProcess.stdout?.on('data', (data: Buffer) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
childProcess.stderr?.on('data', (data: Buffer) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
childProcess.on('exit', (code: number | null) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
try {
|
||||
const result = this.parseAIResponse(output.trim(), currentVersion);
|
||||
this.debug('AI suggestion parsed', result);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
this.debug('Failed to parse AI response', error);
|
||||
// Fallback to simple bump
|
||||
resolve(this.fallbackSuggestion(currentVersion));
|
||||
}
|
||||
} else {
|
||||
this.debug('AI analysis failed', { code, error: errorOutput });
|
||||
// Fallback to simple bump
|
||||
resolve(this.fallbackSuggestion(currentVersion));
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (err: Error) => {
|
||||
this.debug('Process error', err);
|
||||
resolve(this.fallbackSuggestion(currentVersion));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt for Claude to analyze commits and suggest version bump
|
||||
*/
|
||||
private buildPrompt(commits: GitCommit[], currentVersion: string): string {
|
||||
const commitSummary = commits
|
||||
.map((c, i) => `${i + 1}. ${c.shortHash} - ${c.message}`)
|
||||
.join('\n');
|
||||
|
||||
return `You are a semantic versioning expert analyzing git commits to suggest the appropriate version bump.
|
||||
|
||||
Current version: ${currentVersion}
|
||||
|
||||
Analyze these ${commits.length} commits and determine the appropriate semantic version bump:
|
||||
|
||||
${commitSummary}
|
||||
|
||||
Consider:
|
||||
- MAJOR (X.0.0): Breaking changes, API changes, removed features, architectural changes
|
||||
- MINOR (0.X.0): New features, enhancements, additions that maintain backward compatibility
|
||||
- PATCH (0.0.X): Bug fixes, small tweaks, documentation updates, refactoring without new features
|
||||
|
||||
Respond with ONLY a JSON object in this exact format (no markdown, no extra text):
|
||||
{
|
||||
"bumpType": "major|minor|patch",
|
||||
"reason": "Brief explanation of the decision"
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Python script to run Claude analysis
|
||||
*/
|
||||
private createAnalysisScript(prompt: string): string {
|
||||
// Escape the prompt for Python string literal
|
||||
const escapedPrompt = prompt
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n');
|
||||
|
||||
return `
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Use haiku model for fast, cost-effective analysis
|
||||
prompt = "${escapedPrompt}"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["${this.claudePath}", "chat", "--model", "haiku", "--prompt", prompt],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AI response to extract version suggestion
|
||||
*/
|
||||
private parseAIResponse(output: string, currentVersion: string): VersionSuggestion {
|
||||
// Extract JSON from output (Claude might wrap it in markdown or other text)
|
||||
const jsonMatch = output.match(/\{[\s\S]*"bumpType"[\s\S]*"reason"[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error('No JSON found in AI response');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const bumpType = parsed.bumpType as 'major' | 'minor' | 'patch';
|
||||
const reason = parsed.reason || 'AI analysis of commits';
|
||||
|
||||
// Calculate new version
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
let newVersion: string;
|
||||
switch (bumpType) {
|
||||
case 'major':
|
||||
newVersion = `${major + 1}.0.0`;
|
||||
break;
|
||||
case 'minor':
|
||||
newVersion = `${major}.${minor + 1}.0`;
|
||||
break;
|
||||
case 'patch':
|
||||
default:
|
||||
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
version: newVersion,
|
||||
reason,
|
||||
bumpType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback suggestion if AI analysis fails
|
||||
*/
|
||||
private fallbackSuggestion(currentVersion: string): VersionSuggestion {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
return {
|
||||
version: `${major}.${minor}.${patch + 1}`,
|
||||
reason: 'Patch version bump (default)',
|
||||
bumpType: 'patch'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build spawn environment with proper PATH and auth settings
|
||||
*/
|
||||
private buildSpawnEnvironment(): Record<string, string> {
|
||||
const homeDir = os.homedir();
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Build PATH with platform-appropriate separator and locations
|
||||
const pathAdditions = isWindows
|
||||
? [
|
||||
path.join(homeDir, 'AppData', 'Local', 'Programs', 'claude'),
|
||||
path.join(homeDir, 'AppData', 'Roaming', 'npm'),
|
||||
path.join(homeDir, '.local', 'bin'),
|
||||
'C:\\Program Files\\Claude',
|
||||
'C:\\Program Files (x86)\\Claude'
|
||||
]
|
||||
: [
|
||||
'/usr/local/bin',
|
||||
'/opt/homebrew/bin',
|
||||
path.join(homeDir, '.local', 'bin'),
|
||||
path.join(homeDir, 'bin')
|
||||
];
|
||||
|
||||
// Get active Claude profile environment
|
||||
const profileEnv = getProfileEnv();
|
||||
|
||||
const spawnEnv: Record<string, string> = {
|
||||
...process.env as Record<string, string>,
|
||||
...profileEnv,
|
||||
...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }),
|
||||
USER: process.env.USER || process.env.USERNAME || 'user',
|
||||
PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter),
|
||||
PYTHONUNBUFFERED: '1'
|
||||
};
|
||||
|
||||
return spawnEnv;
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,48 @@ export function registerChangelogHandlers(
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION_FROM_COMMITS,
|
||||
async (_, projectId: string, commits: GitCommit[]): Promise<IPCResult<{ version: string; reason: string }>> => {
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
return { success: false, error: 'Project not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current version from existing changelog or git tags
|
||||
const existing = changelogService.readExistingChangelog(project.path);
|
||||
let currentVersion = existing.lastVersion;
|
||||
|
||||
// If no version in changelog, try to get latest tag
|
||||
if (!currentVersion) {
|
||||
const tags = changelogService.getTags(project.path);
|
||||
if (tags.length > 0) {
|
||||
// Extract version from tag name (e.g., "v2.1.0" -> "2.1.0")
|
||||
currentVersion = tags[0].name.replace(/^v/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Use AI to analyze commits and suggest version
|
||||
const result = await changelogService.suggestVersionFromCommits(
|
||||
project.path,
|
||||
commits,
|
||||
currentVersion
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to suggest version from commits'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Changelog Git Operations
|
||||
// ============================================
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||
import type { IPCResult } from '../../../shared/types';
|
||||
import { projectStore } from '../../project-store';
|
||||
|
||||
/**
|
||||
* Register task archive handlers
|
||||
*/
|
||||
export function registerTaskArchiveHandlers(): void {
|
||||
/**
|
||||
* Archive tasks
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.TASK_ARCHIVE,
|
||||
async (
|
||||
_,
|
||||
projectId: string,
|
||||
taskIds: string[],
|
||||
version?: string
|
||||
): Promise<IPCResult<boolean>> => {
|
||||
console.log('[IPC] TASK_ARCHIVE called with projectId:', projectId, 'taskIds:', taskIds);
|
||||
|
||||
const result = projectStore.archiveTasks(projectId, taskIds, version);
|
||||
|
||||
if (result) {
|
||||
console.log('[IPC] TASK_ARCHIVE success');
|
||||
return { success: true, data: true };
|
||||
} else {
|
||||
console.error('[IPC] TASK_ARCHIVE failed');
|
||||
return { success: false, error: 'Failed to archive tasks' };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Unarchive tasks
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.TASK_UNARCHIVE,
|
||||
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<boolean>> => {
|
||||
console.log('[IPC] TASK_UNARCHIVE called with projectId:', projectId, 'taskIds:', taskIds);
|
||||
|
||||
const result = projectStore.unarchiveTasks(projectId, taskIds);
|
||||
|
||||
if (result) {
|
||||
console.log('[IPC] TASK_UNARCHIVE success');
|
||||
return { success: true, data: true };
|
||||
} else {
|
||||
console.error('[IPC] TASK_UNARCHIVE failed');
|
||||
return { success: false, error: 'Failed to unarchive tasks' };
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { registerTaskCRUDHandlers } from './crud-handlers';
|
||||
import { registerTaskExecutionHandlers } from './execution-handlers';
|
||||
import { registerWorktreeHandlers } from './worktree-handlers';
|
||||
import { registerTaskLogsHandlers } from './logs-handlers';
|
||||
import { registerTaskArchiveHandlers } from './archive-handlers';
|
||||
|
||||
/**
|
||||
* Register all task-related IPC handlers
|
||||
@@ -35,6 +36,9 @@ export function registerTaskHandlers(
|
||||
|
||||
// Register logs handlers (get, watch, unwatch)
|
||||
registerTaskLogsHandlers(getMainWindow);
|
||||
|
||||
// Register archive handlers (archive, unarchive)
|
||||
registerTaskArchiveHandlers();
|
||||
}
|
||||
|
||||
// Export shared utilities for use by other modules if needed
|
||||
|
||||
@@ -32,6 +32,10 @@ export interface ChangelogAPI {
|
||||
projectId: string,
|
||||
taskIds: string[]
|
||||
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
||||
suggestChangelogVersionFromCommits: (
|
||||
projectId: string,
|
||||
commits: GitCommit[]
|
||||
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
||||
getChangelogBranches: (projectId: string) => Promise<IPCResult<GitBranchInfo[]>>;
|
||||
getChangelogTags: (projectId: string) => Promise<IPCResult<GitTagInfo[]>>;
|
||||
getChangelogCommitsPreview: (
|
||||
@@ -83,6 +87,12 @@ export const createChangelogAPI = (): ChangelogAPI => ({
|
||||
): Promise<IPCResult<{ version: string; reason: string }>> =>
|
||||
invokeIpc(IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION, projectId, taskIds),
|
||||
|
||||
suggestChangelogVersionFromCommits: (
|
||||
projectId: string,
|
||||
commits: GitCommit[]
|
||||
): Promise<IPCResult<{ version: string; reason: string }>> =>
|
||||
invokeIpc(IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION_FROM_COMMITS, projectId, commits),
|
||||
|
||||
getChangelogBranches: (projectId: string): Promise<IPCResult<GitBranchInfo[]>> =>
|
||||
invokeIpc(IPC_CHANNELS.CHANGELOG_GET_BRANCHES, projectId),
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { TaskCard } from './TaskCard';
|
||||
import { SortableTaskCard } from './SortableTaskCard';
|
||||
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
|
||||
import { cn } from '../lib/utils';
|
||||
import { persistTaskStatus } from '../stores/task-store';
|
||||
import { persistTaskStatus, archiveTasks } from '../stores/task-store';
|
||||
import type { Task, TaskStatus } from '../../shared/types';
|
||||
|
||||
interface KanbanBoardProps {
|
||||
@@ -41,6 +41,7 @@ interface DroppableColumnProps {
|
||||
onTaskClick: (task: Task) => void;
|
||||
isOver: boolean;
|
||||
onAddClick?: () => void;
|
||||
onArchiveAll?: () => void;
|
||||
}
|
||||
|
||||
// Empty state content for each column
|
||||
@@ -84,7 +85,7 @@ const getEmptyStateContent = (status: TaskStatus): { icon: React.ReactNode; mess
|
||||
}
|
||||
};
|
||||
|
||||
function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: DroppableColumnProps) {
|
||||
function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll }: DroppableColumnProps) {
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: status
|
||||
});
|
||||
@@ -130,16 +131,29 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
{status === 'backlog' && onAddClick && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
onClick={onAddClick}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{status === 'backlog' && onAddClick && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
onClick={onAddClick}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{status === 'done' && onArchiveAll && tasks.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 hover:bg-muted-foreground/10 hover:text-muted-foreground transition-colors"
|
||||
onClick={onArchiveAll}
|
||||
title="Archive all done tasks"
|
||||
>
|
||||
<Archive className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
@@ -242,6 +256,20 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
||||
return grouped;
|
||||
}, [filteredTasks]);
|
||||
|
||||
const handleArchiveAll = async () => {
|
||||
// Get projectId from the first task (all tasks should have the same projectId)
|
||||
const projectId = tasks[0]?.projectId;
|
||||
if (!projectId) {
|
||||
console.error('No projectId found');
|
||||
return;
|
||||
}
|
||||
|
||||
const doneTaskIds = tasksByStatus.done.map((t) => t.id);
|
||||
if (doneTaskIds.length === 0) return;
|
||||
|
||||
await archiveTasks(projectId, doneTaskIds);
|
||||
};
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
const task = tasks.find((t) => t.id === active.id);
|
||||
@@ -348,6 +376,7 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
||||
onTaskClick={onTaskClick}
|
||||
isOver={overColumnId === status}
|
||||
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
|
||||
onArchiveAll={status === 'done' ? handleArchiveAll : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
EXECUTION_PHASE_LABELS,
|
||||
EXECUTION_PHASE_BADGE_COLORS
|
||||
} from '../../shared/constants';
|
||||
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview } from '../stores/task-store';
|
||||
import { startTask, stopTask, checkTaskRunning, recoverStuckTask, isIncompleteHumanReview, archiveTasks } from '../stores/task-store';
|
||||
import type { Task, TaskCategory, ExecutionPhase, ReviewReason } from '../../shared/types';
|
||||
|
||||
// Category icon mapping
|
||||
@@ -85,6 +85,11 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
||||
setIsRecovering(false);
|
||||
};
|
||||
|
||||
const handleArchive = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
await archiveTasks(task.projectId, [task.id]);
|
||||
};
|
||||
|
||||
const getStatusBadgeVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'in_progress':
|
||||
@@ -330,6 +335,17 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
||||
<Play className="mr-1.5 h-3 w-3" />
|
||||
Resume
|
||||
</Button>
|
||||
) : task.status === 'done' && !task.metadata?.archivedAt ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2.5 hover:bg-muted-foreground/10"
|
||||
onClick={handleArchive}
|
||||
title="Archive task"
|
||||
>
|
||||
<Archive className="mr-1.5 h-3 w-3" />
|
||||
Archive
|
||||
</Button>
|
||||
) : (task.status === 'backlog' || task.status === 'in_progress') && (
|
||||
<Button
|
||||
variant={isRunning ? 'destructive' : 'default'}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { FileText, Copy, Save, CheckCircle, Image as ImageIcon } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
interface PreviewPanelProps {
|
||||
generatedChangelog: string;
|
||||
@@ -36,11 +39,33 @@ export function PreviewPanel({
|
||||
onDragLeave,
|
||||
onDrop
|
||||
}: PreviewPanelProps) {
|
||||
const [viewMode, setViewMode] = useState<'markdown' | 'preview'>('markdown');
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Preview Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-3">
|
||||
<h2 className="font-medium">Preview</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="font-medium">Preview</h2>
|
||||
<div className="flex items-center gap-1 rounded-md border border-border p-1">
|
||||
<Button
|
||||
variant={viewMode === 'markdown' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('markdown')}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'preview' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('preview')}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -103,14 +128,30 @@ export function PreviewPanel({
|
||||
{imageError}
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className="h-full w-full resize-none font-mono text-sm"
|
||||
value={generatedChangelog}
|
||||
onChange={(e) => onChangelogEdit(e.target.value)}
|
||||
onPaste={onPaste}
|
||||
placeholder="Generated changelog will appear here... (Drag & drop or paste images to add)"
|
||||
/>
|
||||
{viewMode === 'markdown' ? (
|
||||
<div className="flex h-full flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<ImageIcon className="h-3.5 w-3.5" />
|
||||
<span>Paste images into the description to save them to the changelog</span>
|
||||
</div>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className="flex-1 w-full resize-none font-mono text-sm"
|
||||
value={generatedChangelog}
|
||||
onChange={(e) => onChangelogEdit(e.target.value)}
|
||||
onPaste={onPaste}
|
||||
placeholder="Generated changelog will appear here..."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{generatedChangelog}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -119,8 +160,9 @@ export function PreviewPanel({
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Click "Generate Changelog" to create release notes.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
You can drag & drop or paste images (Ctrl+V / Cmd+V) after generating
|
||||
<p className="mt-2 text-xs text-muted-foreground flex items-center justify-center gap-2">
|
||||
<ImageIcon className="h-3.5 w-3.5" />
|
||||
<span>Paste images into the description to save them to the changelog</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,12 @@ export function useChangelog() {
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [versionReason, setVersionReason] = useState<string | null>(null);
|
||||
|
||||
// Initialize changelog preferences from settings on mount
|
||||
const initializeFromSettings = useChangelogStore((state) => state.initializeFromSettings);
|
||||
useEffect(() => {
|
||||
initializeFromSettings();
|
||||
}, [initializeFromSettings]);
|
||||
|
||||
// Load data when project changes
|
||||
useEffect(() => {
|
||||
if (selectedProjectId) {
|
||||
@@ -204,17 +210,32 @@ export function useChangelog() {
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
if (selectedProjectId && selectedTaskIds.length > 0) {
|
||||
if (selectedProjectId) {
|
||||
try {
|
||||
const result = await window.electronAPI.suggestChangelogVersion(
|
||||
selectedProjectId,
|
||||
selectedTaskIds
|
||||
);
|
||||
if (result.success && result.data) {
|
||||
setVersion(result.data.version);
|
||||
setVersionReason(result.data.reason);
|
||||
// Use different version suggestion based on source mode
|
||||
if (sourceMode === 'tasks' && selectedTaskIds.length > 0) {
|
||||
// Task-based: Use rule-based suggester
|
||||
const result = await window.electronAPI.suggestChangelogVersion(
|
||||
selectedProjectId,
|
||||
selectedTaskIds
|
||||
);
|
||||
if (result.success && result.data) {
|
||||
setVersion(result.data.version);
|
||||
setVersionReason(result.data.reason);
|
||||
}
|
||||
} else if ((sourceMode === 'git-history' || sourceMode === 'branch-diff') && previewCommits.length > 0) {
|
||||
// Git-based: Use AI-powered suggester with commits
|
||||
const result = await window.electronAPI.suggestChangelogVersionFromCommits(
|
||||
selectedProjectId,
|
||||
previewCommits
|
||||
);
|
||||
if (result.success && result.data) {
|
||||
setVersion(result.data.version);
|
||||
setVersionReason(result.data.reason);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Failed to suggest version:', error);
|
||||
setVersionReason(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import type {
|
||||
BranchDiffOptions
|
||||
} from '../../shared/types';
|
||||
import { useTaskStore } from './task-store';
|
||||
import { useSettingsStore } from './settings-store';
|
||||
import { saveSettings } from './settings-store';
|
||||
|
||||
interface ChangelogState {
|
||||
// Data
|
||||
@@ -105,6 +107,7 @@ interface ChangelogState {
|
||||
setAudience: (audience: ChangelogAudience) => void;
|
||||
setEmojiLevel: (level: ChangelogEmojiLevel) => void;
|
||||
setCustomInstructions: (instructions: string) => void;
|
||||
initializeFromSettings: () => void;
|
||||
|
||||
// Generation actions
|
||||
setGenerationProgress: (progress: ChangelogGenerationProgress | null) => void;
|
||||
@@ -239,10 +242,27 @@ export const useChangelogStore = create<ChangelogState>((set, get) => ({
|
||||
// Config actions
|
||||
setVersion: (version) => set({ version }),
|
||||
setDate: (date) => set({ date }),
|
||||
setFormat: (format) => set({ format }),
|
||||
setAudience: (audience) => set({ audience }),
|
||||
setEmojiLevel: (level) => set({ emojiLevel: level }),
|
||||
setFormat: (format) => {
|
||||
set({ format });
|
||||
saveSettings({ changelogFormat: format });
|
||||
},
|
||||
setAudience: (audience) => {
|
||||
set({ audience });
|
||||
saveSettings({ changelogAudience: audience });
|
||||
},
|
||||
setEmojiLevel: (level) => {
|
||||
set({ emojiLevel: level });
|
||||
saveSettings({ changelogEmojiLevel: level });
|
||||
},
|
||||
setCustomInstructions: (instructions) => set({ customInstructions: instructions }),
|
||||
initializeFromSettings: () => {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
set({
|
||||
format: settings.changelogFormat || 'keep-a-changelog',
|
||||
audience: settings.changelogAudience || 'user-facing',
|
||||
emojiLevel: settings.changelogEmojiLevel || 'none'
|
||||
});
|
||||
},
|
||||
|
||||
// Generation actions
|
||||
setGenerationProgress: (progress) => set({ generationProgress: progress }),
|
||||
@@ -341,6 +361,11 @@ export async function loadGitData(projectId: string): Promise<void> {
|
||||
if (tagsResult.data.length > 1 && !store.gitHistoryToTag) {
|
||||
store.setGitHistoryToTag(tagsResult.data[1].name);
|
||||
}
|
||||
|
||||
// Auto-set since-version to newest tag if not already set
|
||||
if (tagsResult.data.length > 0 && !store.gitHistorySinceVersion) {
|
||||
store.setGitHistorySinceVersion(tagsResult.data[0].name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
store.setError(error instanceof Error ? error.message : 'Failed to load git data');
|
||||
|
||||
@@ -393,6 +393,37 @@ export async function deleteTask(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive tasks
|
||||
* Marks tasks as archived by adding archivedAt timestamp to metadata
|
||||
*/
|
||||
export async function archiveTasks(
|
||||
projectId: string,
|
||||
taskIds: string[],
|
||||
version?: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await window.electronAPI.archiveTasks(projectId, taskIds, version);
|
||||
|
||||
if (result.success) {
|
||||
// Reload tasks to update the UI (archived tasks will be filtered out by default)
|
||||
await loadTasks(projectId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to archive tasks'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error archiving tasks:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Task Creation Draft Management
|
||||
// ============================================
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* Auto-Build UI Design System - Oscura Midnight Theme */
|
||||
/* A modern, professional design system with deep dark mode and warm yellow accents */
|
||||
|
||||
@@ -26,7 +26,11 @@ export const DEFAULT_APP_SETTINGS = {
|
||||
globalClaudeOAuthToken: undefined as string | undefined,
|
||||
globalOpenAIApiKey: undefined as string | undefined,
|
||||
// Selected agent profile - defaults to 'balanced' for good speed/quality balance
|
||||
selectedAgentProfile: 'balanced'
|
||||
selectedAgentProfile: 'balanced',
|
||||
// Changelog preferences (persisted between sessions)
|
||||
changelogFormat: 'keep-a-changelog' as const,
|
||||
changelogAudience: 'user-facing' as const,
|
||||
changelogEmojiLevel: 'none' as const
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -222,6 +222,7 @@ export const IPC_CHANNELS = {
|
||||
CHANGELOG_SAVE: 'changelog:save',
|
||||
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
|
||||
CHANGELOG_SUGGEST_VERSION: 'changelog:suggestVersion',
|
||||
CHANGELOG_SUGGEST_VERSION_FROM_COMMITS: 'changelog:suggestVersionFromCommits',
|
||||
|
||||
// Changelog git operations (for git-based changelog generation)
|
||||
CHANGELOG_GET_BRANCHES: 'changelog:getBranches',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { NotificationSettings } from './project';
|
||||
import type { ChangelogFormat, ChangelogAudience, ChangelogEmojiLevel } from './changelog';
|
||||
|
||||
// Thinking level for Claude model (budget token allocation)
|
||||
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
||||
@@ -33,6 +34,10 @@ export interface AppSettings {
|
||||
onboardingCompleted?: boolean;
|
||||
// Selected agent profile for preset model/thinking configurations
|
||||
selectedAgentProfile?: string;
|
||||
// Changelog preferences
|
||||
changelogFormat?: ChangelogFormat;
|
||||
changelogAudience?: ChangelogAudience;
|
||||
changelogEmojiLevel?: ChangelogEmojiLevel;
|
||||
}
|
||||
|
||||
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
|
||||
|
||||
Reference in New Issue
Block a user