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-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-virtual": "^3.13.13",
|
"@tanstack/react-virtual": "^3.13.13",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
@@ -58,7 +59,9 @@
|
|||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
|
|||||||
Generated
+885
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ import type {
|
|||||||
GitTagInfo
|
GitTagInfo
|
||||||
} from '../../shared/types';
|
} from '../../shared/types';
|
||||||
import { ChangelogGenerator } from './generator';
|
import { ChangelogGenerator } from './generator';
|
||||||
|
import { VersionSuggester } from './version-suggester';
|
||||||
import { parseExistingChangelog } from './parser';
|
import { parseExistingChangelog } from './parser';
|
||||||
import {
|
import {
|
||||||
getBranches,
|
getBranches,
|
||||||
@@ -38,6 +39,7 @@ export class ChangelogService extends EventEmitter {
|
|||||||
private cachedEnv: Record<string, string> | null = null;
|
private cachedEnv: Record<string, string> | null = null;
|
||||||
private debugEnabled: boolean | null = null;
|
private debugEnabled: boolean | null = null;
|
||||||
private generator: ChangelogGenerator | null = null;
|
private generator: ChangelogGenerator | null = null;
|
||||||
|
private versionSuggester: VersionSuggester | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -240,6 +242,32 @@ export class ChangelogService extends EventEmitter {
|
|||||||
return this.generator;
|
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
|
// 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 {
|
suggestVersion(specs: TaskSpecContent[], currentVersion?: string): string {
|
||||||
// Default starting version
|
// Default starting version
|
||||||
@@ -445,7 +473,7 @@ export class ChangelogService extends EventEmitter {
|
|||||||
return '1.0.0';
|
return '1.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
let [major, minor, patch] = parts;
|
const [major, minor, patch] = parts;
|
||||||
|
|
||||||
// Analyze specs for version increment decision
|
// Analyze specs for version increment decision
|
||||||
let hasBreakingChanges = false;
|
let hasBreakingChanges = false;
|
||||||
@@ -473,6 +501,46 @@ export class ChangelogService extends EventEmitter {
|
|||||||
return `${major}.${minor}.${patch + 1}`;
|
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
|
// Export singleton instance
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const FORMAT_TEMPLATES = {
|
|||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- [List fixes]`,
|
- [List fixes]`,
|
||||||
|
|
||||||
'github-release': (version: string) => `## What's New in v${version}
|
'github-release': (version: string, date: string) => `## ${version} - ${date}
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@@ -223,9 +223,9 @@ export function buildGitPrompt(
|
|||||||
let formatSpecificInstructions = '';
|
let formatSpecificInstructions = '';
|
||||||
if (request.format === 'github-release') {
|
if (request.format === 'github-release') {
|
||||||
formatSpecificInstructions = `
|
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
|
- Use category sections: New Features, Improvements, Bug Fixes, Documentation, Other Changes
|
||||||
- ONLY include sections that have actual changes - skip empty sections entirely
|
- ONLY include sections that have actual changes - skip empty sections entirely
|
||||||
- Add a blank line between each bullet point for cleaner formatting
|
- Add a blank line between each bullet point for cleaner formatting
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Architecture:
|
* Architecture:
|
||||||
* - changelog-service.ts: Main service facade (orchestrates all operations)
|
* - changelog-service.ts: Main service facade (orchestrates all operations)
|
||||||
* - generator.ts: AI-powered changelog generation
|
* - generator.ts: AI-powered changelog generation
|
||||||
|
* - version-suggester.ts: AI-powered version bump suggestions
|
||||||
* - parser.ts: Changelog and spec parsing logic
|
* - parser.ts: Changelog and spec parsing logic
|
||||||
* - formatter.ts: Prompt building and formatting
|
* - formatter.ts: Prompt building and formatting
|
||||||
* - git-integration.ts: Git operations (branches, tags, commits)
|
* - git-integration.ts: Git operations (branches, tags, commits)
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
|
|
||||||
export { ChangelogService, changelogService } from './changelog-service';
|
export { ChangelogService, changelogService } from './changelog-service';
|
||||||
export { ChangelogGenerator } from './generator';
|
export { ChangelogGenerator } from './generator';
|
||||||
|
export { VersionSuggester } from './version-suggester';
|
||||||
export * from './parser';
|
export * from './parser';
|
||||||
export * from './formatter';
|
export * from './formatter';
|
||||||
export * from './git-integration';
|
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
|
// 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 { registerTaskExecutionHandlers } from './execution-handlers';
|
||||||
import { registerWorktreeHandlers } from './worktree-handlers';
|
import { registerWorktreeHandlers } from './worktree-handlers';
|
||||||
import { registerTaskLogsHandlers } from './logs-handlers';
|
import { registerTaskLogsHandlers } from './logs-handlers';
|
||||||
|
import { registerTaskArchiveHandlers } from './archive-handlers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all task-related IPC handlers
|
* Register all task-related IPC handlers
|
||||||
@@ -35,6 +36,9 @@ export function registerTaskHandlers(
|
|||||||
|
|
||||||
// Register logs handlers (get, watch, unwatch)
|
// Register logs handlers (get, watch, unwatch)
|
||||||
registerTaskLogsHandlers(getMainWindow);
|
registerTaskLogsHandlers(getMainWindow);
|
||||||
|
|
||||||
|
// Register archive handlers (archive, unarchive)
|
||||||
|
registerTaskArchiveHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export shared utilities for use by other modules if needed
|
// Export shared utilities for use by other modules if needed
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export interface ChangelogAPI {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
taskIds: string[]
|
taskIds: string[]
|
||||||
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
||||||
|
suggestChangelogVersionFromCommits: (
|
||||||
|
projectId: string,
|
||||||
|
commits: GitCommit[]
|
||||||
|
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
||||||
getChangelogBranches: (projectId: string) => Promise<IPCResult<GitBranchInfo[]>>;
|
getChangelogBranches: (projectId: string) => Promise<IPCResult<GitBranchInfo[]>>;
|
||||||
getChangelogTags: (projectId: string) => Promise<IPCResult<GitTagInfo[]>>;
|
getChangelogTags: (projectId: string) => Promise<IPCResult<GitTagInfo[]>>;
|
||||||
getChangelogCommitsPreview: (
|
getChangelogCommitsPreview: (
|
||||||
@@ -83,6 +87,12 @@ export const createChangelogAPI = (): ChangelogAPI => ({
|
|||||||
): Promise<IPCResult<{ version: string; reason: string }>> =>
|
): Promise<IPCResult<{ version: string; reason: string }>> =>
|
||||||
invokeIpc(IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION, projectId, taskIds),
|
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[]>> =>
|
getChangelogBranches: (projectId: string): Promise<IPCResult<GitBranchInfo[]>> =>
|
||||||
invokeIpc(IPC_CHANNELS.CHANGELOG_GET_BRANCHES, projectId),
|
invokeIpc(IPC_CHANNELS.CHANGELOG_GET_BRANCHES, projectId),
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { TaskCard } from './TaskCard';
|
|||||||
import { SortableTaskCard } from './SortableTaskCard';
|
import { SortableTaskCard } from './SortableTaskCard';
|
||||||
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
|
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
|
||||||
import { cn } from '../lib/utils';
|
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';
|
import type { Task, TaskStatus } from '../../shared/types';
|
||||||
|
|
||||||
interface KanbanBoardProps {
|
interface KanbanBoardProps {
|
||||||
@@ -41,6 +41,7 @@ interface DroppableColumnProps {
|
|||||||
onTaskClick: (task: Task) => void;
|
onTaskClick: (task: Task) => void;
|
||||||
isOver: boolean;
|
isOver: boolean;
|
||||||
onAddClick?: () => void;
|
onAddClick?: () => void;
|
||||||
|
onArchiveAll?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty state content for each column
|
// 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({
|
const { setNodeRef } = useDroppable({
|
||||||
id: status
|
id: status
|
||||||
});
|
});
|
||||||
@@ -130,16 +131,29 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
|
|||||||
{tasks.length}
|
{tasks.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{status === 'backlog' && onAddClick && (
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
{status === 'backlog' && onAddClick && (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
|
size="icon"
|
||||||
onClick={onAddClick}
|
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
|
||||||
>
|
onClick={onAddClick}
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Task list */}
|
{/* Task list */}
|
||||||
@@ -242,6 +256,20 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
|||||||
return grouped;
|
return grouped;
|
||||||
}, [filteredTasks]);
|
}, [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 handleDragStart = (event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
const task = tasks.find((t) => t.id === active.id);
|
const task = tasks.find((t) => t.id === active.id);
|
||||||
@@ -348,6 +376,7 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
|||||||
onTaskClick={onTaskClick}
|
onTaskClick={onTaskClick}
|
||||||
isOver={overColumnId === status}
|
isOver={overColumnId === status}
|
||||||
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
|
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
|
||||||
|
onArchiveAll={status === 'done' ? handleArchiveAll : undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
EXECUTION_PHASE_LABELS,
|
EXECUTION_PHASE_LABELS,
|
||||||
EXECUTION_PHASE_BADGE_COLORS
|
EXECUTION_PHASE_BADGE_COLORS
|
||||||
} from '../../shared/constants';
|
} 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';
|
import type { Task, TaskCategory, ExecutionPhase, ReviewReason } from '../../shared/types';
|
||||||
|
|
||||||
// Category icon mapping
|
// Category icon mapping
|
||||||
@@ -85,6 +85,11 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
|||||||
setIsRecovering(false);
|
setIsRecovering(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleArchive = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await archiveTasks(task.projectId, [task.id]);
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusBadgeVariant = (status: string) => {
|
const getStatusBadgeVariant = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'in_progress':
|
case 'in_progress':
|
||||||
@@ -330,6 +335,17 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
|||||||
<Play className="mr-1.5 h-3 w-3" />
|
<Play className="mr-1.5 h-3 w-3" />
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</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') && (
|
) : (task.status === 'backlog' || task.status === 'in_progress') && (
|
||||||
<Button
|
<Button
|
||||||
variant={isRunning ? 'destructive' : 'default'}
|
variant={isRunning ? 'destructive' : 'default'}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { FileText, Copy, Save, CheckCircle, Image as ImageIcon } from 'lucide-react';
|
import { FileText, Copy, Save, CheckCircle, Image as ImageIcon } from 'lucide-react';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
interface PreviewPanelProps {
|
interface PreviewPanelProps {
|
||||||
generatedChangelog: string;
|
generatedChangelog: string;
|
||||||
@@ -36,11 +39,33 @@ export function PreviewPanel({
|
|||||||
onDragLeave,
|
onDragLeave,
|
||||||
onDrop
|
onDrop
|
||||||
}: PreviewPanelProps) {
|
}: PreviewPanelProps) {
|
||||||
|
const [viewMode, setViewMode] = useState<'markdown' | 'preview'>('markdown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Preview Header */}
|
{/* Preview Header */}
|
||||||
<div className="flex items-center justify-between border-b border-border px-6 py-3">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -103,14 +128,30 @@ export function PreviewPanel({
|
|||||||
{imageError}
|
{imageError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Textarea
|
{viewMode === 'markdown' ? (
|
||||||
ref={textareaRef}
|
<div className="flex h-full flex-col gap-2">
|
||||||
className="h-full w-full resize-none font-mono text-sm"
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
value={generatedChangelog}
|
<ImageIcon className="h-3.5 w-3.5" />
|
||||||
onChange={(e) => onChangelogEdit(e.target.value)}
|
<span>Paste images into the description to save them to the changelog</span>
|
||||||
onPaste={onPaste}
|
</div>
|
||||||
placeholder="Generated changelog will appear here... (Drag & drop or paste images to add)"
|
<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">
|
<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">
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
Click "Generate Changelog" to create release notes.
|
Click "Generate Changelog" to create release notes.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-xs text-muted-foreground flex items-center justify-center gap-2">
|
||||||
You can drag & drop or paste images (Ctrl+V / Cmd+V) after generating
|
<ImageIcon className="h-3.5 w-3.5" />
|
||||||
|
<span>Paste images into the description to save them to the changelog</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export function useChangelog() {
|
|||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
const [versionReason, setVersionReason] = useState<string | null>(null);
|
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
|
// Load data when project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProjectId) {
|
if (selectedProjectId) {
|
||||||
@@ -204,17 +210,32 @@ export function useChangelog() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleContinue = async () => {
|
const handleContinue = async () => {
|
||||||
if (selectedProjectId && selectedTaskIds.length > 0) {
|
if (selectedProjectId) {
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.suggestChangelogVersion(
|
// Use different version suggestion based on source mode
|
||||||
selectedProjectId,
|
if (sourceMode === 'tasks' && selectedTaskIds.length > 0) {
|
||||||
selectedTaskIds
|
// Task-based: Use rule-based suggester
|
||||||
);
|
const result = await window.electronAPI.suggestChangelogVersion(
|
||||||
if (result.success && result.data) {
|
selectedProjectId,
|
||||||
setVersion(result.data.version);
|
selectedTaskIds
|
||||||
setVersionReason(result.data.reason);
|
);
|
||||||
|
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);
|
setVersionReason(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import type {
|
|||||||
BranchDiffOptions
|
BranchDiffOptions
|
||||||
} from '../../shared/types';
|
} from '../../shared/types';
|
||||||
import { useTaskStore } from './task-store';
|
import { useTaskStore } from './task-store';
|
||||||
|
import { useSettingsStore } from './settings-store';
|
||||||
|
import { saveSettings } from './settings-store';
|
||||||
|
|
||||||
interface ChangelogState {
|
interface ChangelogState {
|
||||||
// Data
|
// Data
|
||||||
@@ -105,6 +107,7 @@ interface ChangelogState {
|
|||||||
setAudience: (audience: ChangelogAudience) => void;
|
setAudience: (audience: ChangelogAudience) => void;
|
||||||
setEmojiLevel: (level: ChangelogEmojiLevel) => void;
|
setEmojiLevel: (level: ChangelogEmojiLevel) => void;
|
||||||
setCustomInstructions: (instructions: string) => void;
|
setCustomInstructions: (instructions: string) => void;
|
||||||
|
initializeFromSettings: () => void;
|
||||||
|
|
||||||
// Generation actions
|
// Generation actions
|
||||||
setGenerationProgress: (progress: ChangelogGenerationProgress | null) => void;
|
setGenerationProgress: (progress: ChangelogGenerationProgress | null) => void;
|
||||||
@@ -239,10 +242,27 @@ export const useChangelogStore = create<ChangelogState>((set, get) => ({
|
|||||||
// Config actions
|
// Config actions
|
||||||
setVersion: (version) => set({ version }),
|
setVersion: (version) => set({ version }),
|
||||||
setDate: (date) => set({ date }),
|
setDate: (date) => set({ date }),
|
||||||
setFormat: (format) => set({ format }),
|
setFormat: (format) => {
|
||||||
setAudience: (audience) => set({ audience }),
|
set({ format });
|
||||||
setEmojiLevel: (level) => set({ emojiLevel: level }),
|
saveSettings({ changelogFormat: format });
|
||||||
|
},
|
||||||
|
setAudience: (audience) => {
|
||||||
|
set({ audience });
|
||||||
|
saveSettings({ changelogAudience: audience });
|
||||||
|
},
|
||||||
|
setEmojiLevel: (level) => {
|
||||||
|
set({ emojiLevel: level });
|
||||||
|
saveSettings({ changelogEmojiLevel: level });
|
||||||
|
},
|
||||||
setCustomInstructions: (instructions) => set({ customInstructions: instructions }),
|
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
|
// Generation actions
|
||||||
setGenerationProgress: (progress) => set({ generationProgress: progress }),
|
setGenerationProgress: (progress) => set({ generationProgress: progress }),
|
||||||
@@ -341,6 +361,11 @@ export async function loadGitData(projectId: string): Promise<void> {
|
|||||||
if (tagsResult.data.length > 1 && !store.gitHistoryToTag) {
|
if (tagsResult.data.length > 1 && !store.gitHistoryToTag) {
|
||||||
store.setGitHistoryToTag(tagsResult.data[1].name);
|
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) {
|
} catch (error) {
|
||||||
store.setError(error instanceof Error ? error.message : 'Failed to load git data');
|
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
|
// Task Creation Draft Management
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
/* Auto-Build UI Design System - Oscura Midnight Theme */
|
/* Auto-Build UI Design System - Oscura Midnight Theme */
|
||||||
/* A modern, professional design system with deep dark mode and warm yellow accents */
|
/* 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,
|
globalClaudeOAuthToken: undefined as string | undefined,
|
||||||
globalOpenAIApiKey: undefined as string | undefined,
|
globalOpenAIApiKey: undefined as string | undefined,
|
||||||
// Selected agent profile - defaults to 'balanced' for good speed/quality balance
|
// 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_SAVE: 'changelog:save',
|
||||||
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
|
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
|
||||||
CHANGELOG_SUGGEST_VERSION: 'changelog:suggestVersion',
|
CHANGELOG_SUGGEST_VERSION: 'changelog:suggestVersion',
|
||||||
|
CHANGELOG_SUGGEST_VERSION_FROM_COMMITS: 'changelog:suggestVersionFromCommits',
|
||||||
|
|
||||||
// Changelog git operations (for git-based changelog generation)
|
// Changelog git operations (for git-based changelog generation)
|
||||||
CHANGELOG_GET_BRANCHES: 'changelog:getBranches',
|
CHANGELOG_GET_BRANCHES: 'changelog:getBranches',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NotificationSettings } from './project';
|
import type { NotificationSettings } from './project';
|
||||||
|
import type { ChangelogFormat, ChangelogAudience, ChangelogEmojiLevel } from './changelog';
|
||||||
|
|
||||||
// Thinking level for Claude model (budget token allocation)
|
// Thinking level for Claude model (budget token allocation)
|
||||||
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
||||||
@@ -33,6 +34,10 @@ export interface AppSettings {
|
|||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
// Selected agent profile for preset model/thinking configurations
|
// Selected agent profile for preset model/thinking configurations
|
||||||
selectedAgentProfile?: string;
|
selectedAgentProfile?: string;
|
||||||
|
// Changelog preferences
|
||||||
|
changelogFormat?: ChangelogFormat;
|
||||||
|
changelogAudience?: ChangelogAudience;
|
||||||
|
changelogEmojiLevel?: ChangelogEmojiLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
|
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
|
||||||
|
|||||||
Reference in New Issue
Block a user