Version 2.0.1

This commit is contained in:
AndyMik90
2025-12-15 18:28:59 +01:00
parent f9ef7eae09
commit 4b242c4a64
15 changed files with 442 additions and 25 deletions
+18
View File
@@ -1,3 +1,21 @@
## What's New in 2.0.1
### 🚀 New Features
- **Update Check with Release URLs**: Enhanced update checking functionality to include release URLs, allowing users to easily access release information
- **Markdown Renderer for Release Notes**: Added markdown renderer in advanced settings to properly display formatted release notes
- **Terminal Name Generator**: New feature for generating terminal names
### 🔧 Improvements
- **LLM Provider Naming**: Updated project settings to reflect new LLM provider name
- **IPC Handlers**: Improved IPC handlers for external link management
- **UI Simplification**: Refactored App component to simplify project selection display by removing unnecessary wrapper elements
- **Docker Infrastructure**: Updated FalkorDB service container naming in docker-compose configuration
- **Documentation**: Improved README with dedicated CLI documentation and infrastructure status information
### 📚 Documentation
- Enhanced README with comprehensive CLI documentation and setup instructions
- Added Docker infrastructure status documentation
## What's New in v2.0.0
### New Features
@@ -52,6 +52,43 @@ const AUDIENCE_INSTRUCTIONS = {
'marketing': 'You are a marketing specialist writing release notes. Focus on outcomes and user impact with compelling language.'
};
/**
* Get emoji usage instructions based on level
*/
function getEmojiInstructions(emojiLevel?: string): string {
if (!emojiLevel || emojiLevel === 'none') {
return '';
}
const instructions: Record<string, string> = {
'little': `Add emojis ONLY to section headings. Each heading should have one contextual emoji at the start.
Examples:
- "### ✨ New Features" or "### 🚀 New Features"
- "### 🐛 Bug Fixes"
- "### 🔧 Improvements" or "### ⚡ Improvements"
- "### 📚 Documentation"
Do NOT add emojis to individual line items.`,
'medium': `Add emojis to section headings AND to notable/important items only.
Section headings should have one emoji (e.g., "### ✨ New Features", "### 🐛 Bug Fixes").
Add emojis to 2-3 highlighted items per section that are particularly significant.
Examples of highlighted items:
- "- 🎉 **Major Feature**: Description"
- "- 🔒 **Security Fix**: Description"
Most regular line items should NOT have emojis.`,
'high': `Add emojis to section headings AND every line item for maximum visual appeal.
Section headings: "### ✨ New Features", "### 🐛 Bug Fixes", "### ⚡ Improvements"
Every line item should start with a contextual emoji:
- "- ✨ Added new feature..."
- "- 🐛 Fixed bug where..."
- "- 🔧 Improved performance of..."
- "- 📝 Updated documentation for..."
- "- 🎨 Refined UI styling..."
Use diverse, contextually appropriate emojis for each item.`
};
return instructions[emojiLevel] || '';
}
/**
* Build changelog prompt from task specs
*/
@@ -61,6 +98,7 @@ export function buildChangelogPrompt(
): string {
const audienceInstruction = AUDIENCE_INSTRUCTIONS[request.audience];
const formatInstruction = FORMAT_TEMPLATES[request.format](request.version, request.date);
const emojiInstruction = getEmojiInstructions(request.emojiLevel);
// Build CONCISE task summaries (key to avoiding timeout)
const taskSummaries = specs.map(spec => {
@@ -86,6 +124,7 @@ export function buildChangelogPrompt(
Format:
${formatInstruction}
${emojiInstruction ? `\nEmoji Usage:\n${emojiInstruction}` : ''}
Completed tasks:
${taskSummaries}
@@ -104,6 +143,7 @@ export function buildGitPrompt(
): string {
const audienceInstruction = AUDIENCE_INSTRUCTIONS[request.audience];
const formatInstruction = FORMAT_TEMPLATES[request.format](request.version, request.date);
const emojiInstruction = getEmojiInstructions(request.emojiLevel);
// Format commits for the prompt
// Group by conventional commit type if detected
@@ -155,6 +195,7 @@ Conventional commit types to recognize:
Format:
${formatInstruction}
${emojiInstruction ? `\nEmoji Usage:\n${emojiInstruction}` : ''}
Git commits (${commits.length} total):
${commitLines}
@@ -1,7 +1,7 @@
import { ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import path from 'path';
import { existsSync, readFileSync } from 'fs';
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
import { execSync } from 'child_process';
import { IPC_CHANNELS, getSpecsDir } from '../../shared/constants';
import type {
@@ -292,6 +292,48 @@ export function registerChangelogHandlers(
}
);
// ============================================
// Changelog Image Operations
// ============================================
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_SAVE_IMAGE,
async (_, projectId: string, imageData: string, filename: string): Promise<IPCResult<{ relativePath: string; url: string }>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
// Create .github/assets directory if it doesn't exist
const assetsDir = path.join(project.path, '.github', 'assets');
if (!existsSync(assetsDir)) {
mkdirSync(assetsDir, { recursive: true });
}
// Decode base64 image data
const base64Data = imageData.includes(',') ? imageData.split(',')[1] : imageData;
const buffer = Buffer.from(base64Data, 'base64');
// Save image file
const imagePath = path.join(assetsDir, filename);
writeFileSync(imagePath, buffer);
// Return relative path for use in markdown
const relativePath = `.github/assets/${filename}`;
// For GitHub releases, we'll use the relative path which will work when the release is created
const url = relativePath;
return { success: true, data: { relativePath, url } };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to save image'
};
}
}
);
// ============================================
// Changelog Agent Events → Renderer
}
@@ -324,8 +324,8 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join('\n'
};
writeFileSync(path.join(specDir, 'task_metadata.json'), JSON.stringify(metadata, null, 2));
// Start spec creation with the existing spec directory
agentManager.startSpecCreation(specId, project.path, taskDescription, specDir, metadata);
// NOTE: We do NOT auto-start spec creation here - user should explicitly start the task
// from the kanban board when they're ready
// Update feature with linked spec
feature.status = 'planned';
@@ -121,6 +121,11 @@ export interface AgentAPI {
options: GitHistoryOptions | BranchDiffOptions,
mode: 'git-history' | 'branch-diff'
) => Promise<IPCResult<GitCommit[]>>;
saveChangelogImage: (
projectId: string,
imageData: string,
filename: string
) => Promise<IPCResult<{ relativePath: string; url: string }>>;
// Changelog Event Listeners
onChangelogGenerationProgress: (callback: (projectId: string, progress: ChangelogGenerationProgress) => void) => () => void;
@@ -491,6 +496,13 @@ export const createAgentAPI = (): AgentAPI => ({
): Promise<IPCResult<GitCommit[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_GET_COMMITS_PREVIEW, projectId, options, mode),
saveChangelogImage: (
projectId: string,
imageData: string,
filename: string
): Promise<IPCResult<{ relativePath: string; url: string }>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_SAVE_IMAGE, projectId, imageData, filename),
// Changelog Event Listeners
onChangelogGenerationProgress: (
callback: (projectId: string, progress: ChangelogGenerationProgress) => void
+1 -1
View File
@@ -285,7 +285,7 @@ export function App() {
/>
</div>
{activeView === 'roadmap' && selectedProjectId && (
<Roadmap projectId={selectedProjectId} />
<Roadmap projectId={selectedProjectId} onGoToTask={handleGoToTask} />
)}
{activeView === 'context' && selectedProjectId && (
<Context projectId={selectedProjectId} />
@@ -14,7 +14,8 @@ import {
BarChart3,
Clock,
AlertCircle,
Play
Play,
ExternalLink
} from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
@@ -44,11 +45,13 @@ import type { RoadmapFeature, RoadmapPhase } from '../../shared/types';
interface RoadmapProps {
projectId: string;
onGoToTask?: (taskId: string) => void;
}
export function Roadmap({ projectId }: RoadmapProps) {
export function Roadmap({ projectId, onGoToTask }: RoadmapProps) {
const roadmap = useRoadmapStore((state) => state.roadmap);
const generationStatus = useRoadmapStore((state) => state.generationStatus);
const updateFeatureLinkedSpec = useRoadmapStore((state) => state.updateFeatureLinkedSpec);
const [selectedFeature, setSelectedFeature] = useState<RoadmapFeature | null>(null);
const [activeTab, setActiveTab] = useState('phases');
@@ -67,8 +70,23 @@ export function Roadmap({ projectId }: RoadmapProps) {
const handleConvertToSpec = async (feature: RoadmapFeature) => {
const result = await window.electronAPI.convertFeatureToSpec(projectId, feature.id);
if (result.success) {
// Feature converted to spec - could show notification
if (result.success && result.data) {
// Update the store with the linked spec
updateFeatureLinkedSpec(feature.id, result.data.specId);
// Update the selected feature if it's the one that was converted
if (selectedFeature?.id === feature.id) {
setSelectedFeature({
...feature,
linkedSpecId: result.data.specId,
status: 'planned'
});
}
}
};
const handleGoToTask = (specId: string) => {
if (onGoToTask) {
onGoToTask(specId);
}
};
@@ -203,6 +221,7 @@ export function Roadmap({ projectId }: RoadmapProps) {
isFirst={index === 0}
onFeatureSelect={setSelectedFeature}
onConvertToSpec={handleConvertToSpec}
onGoToTask={handleGoToTask}
/>
))}
</div>
@@ -217,6 +236,7 @@ export function Roadmap({ projectId }: RoadmapProps) {
feature={feature}
onClick={() => setSelectedFeature(feature)}
onConvertToSpec={handleConvertToSpec}
onGoToTask={handleGoToTask}
/>
))}
</div>
@@ -273,6 +293,7 @@ export function Roadmap({ projectId }: RoadmapProps) {
feature={selectedFeature}
onClose={() => setSelectedFeature(null)}
onConvertToSpec={handleConvertToSpec}
onGoToTask={handleGoToTask}
/>
)}
</div>
@@ -286,9 +307,10 @@ interface PhaseCardProps {
isFirst: boolean;
onFeatureSelect: (feature: RoadmapFeature) => void;
onConvertToSpec: (feature: RoadmapFeature) => void;
onGoToTask: (specId: string) => void;
}
function PhaseCard({ phase, features, isFirst, onFeatureSelect, onConvertToSpec }: PhaseCardProps) {
function PhaseCard({ phase, features, isFirst, onFeatureSelect, onConvertToSpec, onGoToTask }: PhaseCardProps) {
const completedCount = features.filter((f) => f.status === 'done').length;
const progress = features.length > 0 ? (completedCount / features.length) * 100 : 0;
@@ -378,7 +400,18 @@ function PhaseCard({ phase, features, isFirst, onFeatureSelect, onConvertToSpec
{feature.status === 'done' ? (
<CheckCircle2 className="h-4 w-4 text-success" />
) : feature.linkedSpecId ? (
<Badge variant="outline" className="text-xs">In Progress</Badge>
<Button
variant="ghost"
size="sm"
className="h-6 px-2"
onClick={(e) => {
e.stopPropagation();
onGoToTask(feature.linkedSpecId!);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />
View Task
</Button>
) : (
<Button
variant="ghost"
@@ -411,9 +444,10 @@ interface FeatureCardProps {
feature: RoadmapFeature;
onClick: () => void;
onConvertToSpec: (feature: RoadmapFeature) => void;
onGoToTask: (specId: string) => void;
}
function FeatureCard({ feature, onClick, onConvertToSpec }: FeatureCardProps) {
function FeatureCard({ feature, onClick, onConvertToSpec, onGoToTask }: FeatureCardProps) {
return (
<Card
className="p-4 hover:bg-muted/50 cursor-pointer transition-colors"
@@ -438,7 +472,19 @@ function FeatureCard({ feature, onClick, onConvertToSpec }: FeatureCardProps) {
<h3 className="font-medium">{feature.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">{feature.description}</p>
</div>
{!feature.linkedSpecId && feature.status !== 'done' && (
{feature.linkedSpecId ? (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
onGoToTask(feature.linkedSpecId!);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />
Go to Task
</Button>
) : feature.status !== 'done' && (
<Button
variant="outline"
size="sm"
@@ -461,9 +507,10 @@ interface FeatureDetailPanelProps {
feature: RoadmapFeature;
onClose: () => void;
onConvertToSpec: (feature: RoadmapFeature) => void;
onGoToTask: (specId: string) => void;
}
function FeatureDetailPanel({ feature, onClose, onConvertToSpec }: FeatureDetailPanelProps) {
function FeatureDetailPanel({ feature, onClose, onConvertToSpec, onGoToTask }: FeatureDetailPanelProps) {
return (
<div className="fixed inset-y-0 right-0 w-96 bg-card border-l border-border shadow-lg flex flex-col z-50">
{/* Header */}
@@ -580,7 +627,14 @@ function FeatureDetailPanel({ feature, onClose, onConvertToSpec }: FeatureDetail
</div>
{/* Actions */}
{!feature.linkedSpecId && feature.status !== 'done' && (
{feature.linkedSpecId ? (
<div className="flex-shrink-0 p-4 border-t border-border">
<Button className="w-full" onClick={() => onGoToTask(feature.linkedSpecId!)}>
<ExternalLink className="h-4 w-4 mr-2" />
Go to Task
</Button>
</div>
) : feature.status !== 'done' && (
<div className="flex-shrink-0 p-4 border-t border-border">
<Button className="w-full" onClick={() => onConvertToSpec(feature)}>
<Zap className="h-4 w-4 mr-2" />
@@ -34,6 +34,7 @@ export function Changelog() {
date,
format,
audience,
emojiLevel,
customInstructions,
generationProgress,
generatedChangelog,
@@ -65,6 +66,7 @@ export function Changelog() {
setDate,
setFormat,
setAudience,
setEmojiLevel,
setCustomInstructions,
updateGeneratedChangelog,
setShowAdvanced,
@@ -156,6 +158,7 @@ export function Changelog() {
date={date}
format={format}
audience={audience}
emojiLevel={emojiLevel}
customInstructions={customInstructions}
generationProgress={generationProgress}
generatedChangelog={generatedChangelog}
@@ -171,6 +174,7 @@ export function Changelog() {
onDateChange={setDate}
onFormatChange={setFormat}
onAudienceChange={setAudience}
onEmojiLevelChange={setEmojiLevel}
onCustomInstructionsChange={setCustomInstructions}
onShowAdvancedChange={setShowAdvanced}
onGenerate={handleGenerate}
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { ArrowLeft, FileText, GitCommit, Sparkles, RefreshCw, AlertCircle, ChevronUp, ChevronDown, Copy, Save, CheckCircle, PartyPopper, Github, Archive, ExternalLink, Check } from 'lucide-react';
import { useState, useCallback, useRef, type DragEvent, type ClipboardEvent } from 'react';
import { ArrowLeft, FileText, GitCommit, Sparkles, RefreshCw, AlertCircle, ChevronUp, ChevronDown, Copy, Save, CheckCircle, PartyPopper, Github, Archive, ExternalLink, Check, Image as ImageIcon } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Input } from '../ui/input';
@@ -14,11 +14,17 @@ import {
CHANGELOG_FORMAT_DESCRIPTIONS,
CHANGELOG_AUDIENCE_LABELS,
CHANGELOG_AUDIENCE_DESCRIPTIONS,
CHANGELOG_STAGE_LABELS
CHANGELOG_EMOJI_LEVEL_LABELS,
CHANGELOG_EMOJI_LEVEL_DESCRIPTIONS,
CHANGELOG_STAGE_LABELS,
ALLOWED_IMAGE_TYPES_DISPLAY
} from '../../../shared/constants';
import { blobToBase64, isValidImageMimeType, resolveFilename } from '../ImageUpload';
import { useProjectStore } from '../../stores/project-store';
import type {
ChangelogFormat,
ChangelogAudience,
ChangelogEmojiLevel,
ChangelogTask,
ChangelogSourceMode,
GitCommit as GitCommitType
@@ -35,6 +41,7 @@ interface Step2ConfigureGenerateProps {
date: string;
format: ChangelogFormat;
audience: ChangelogAudience;
emojiLevel: ChangelogEmojiLevel;
customInstructions: string;
generationProgress: { stage: string; progress: number; message?: string; error?: string } | null;
generatedChangelog: string;
@@ -50,6 +57,7 @@ interface Step2ConfigureGenerateProps {
onDateChange: (d: string) => void;
onFormatChange: (f: ChangelogFormat) => void;
onAudienceChange: (a: ChangelogAudience) => void;
onEmojiLevelChange: (l: ChangelogEmojiLevel) => void;
onCustomInstructionsChange: (i: string) => void;
onShowAdvancedChange: (show: boolean) => void;
onGenerate: () => void;
@@ -69,6 +77,7 @@ export function Step2ConfigureGenerate({
date,
format,
audience,
emojiLevel,
customInstructions,
generationProgress,
generatedChangelog,
@@ -84,6 +93,7 @@ export function Step2ConfigureGenerate({
onDateChange,
onFormatChange,
onAudienceChange,
onEmojiLevelChange,
onCustomInstructionsChange,
onShowAdvancedChange,
onGenerate,
@@ -92,6 +102,142 @@ export function Step2ConfigureGenerate({
onChangelogEdit
}: Step2ConfigureGenerateProps) {
const selectedTasks = doneTasks.filter((t) => selectedTaskIds.includes(t.id));
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
const [isDragOver, setIsDragOver] = useState(false);
const [imageError, setImageError] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Handle image paste
const handlePaste = useCallback(async (e: ClipboardEvent<HTMLTextAreaElement>) => {
if (!selectedProjectId) return;
const items = Array.from(e.clipboardData.items);
const imageItems = items.filter((item) => item.type.startsWith('image/'));
if (imageItems.length === 0) return;
e.preventDefault();
setImageError(null);
for (const item of imageItems) {
const file = item.getAsFile();
if (!file) continue;
if (!isValidImageMimeType(file.type)) {
setImageError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`);
continue;
}
try {
const dataUrl = await blobToBase64(file);
const extension = file.type.split('/')[1] || 'png';
const timestamp = Date.now();
const baseFilename = `changelog-${timestamp}.${extension}`;
const filename = resolveFilename(baseFilename, []);
const result = await window.electronAPI.saveChangelogImage(
selectedProjectId,
dataUrl,
filename
);
if (result.success && result.data) {
// Insert markdown image at cursor position
const textarea = textareaRef.current;
if (textarea) {
const cursorPos = textarea.selectionStart;
const textBefore = generatedChangelog.substring(0, cursorPos);
const textAfter = generatedChangelog.substring(cursorPos);
const imageMarkdown = `\n![${filename}](${result.data.relativePath})\n`;
onChangelogEdit(textBefore + imageMarkdown + textAfter);
// Set cursor position after inserted image
setTimeout(() => {
const newPos = cursorPos + imageMarkdown.length;
textarea.setSelectionRange(newPos, newPos);
textarea.focus();
}, 0);
}
} else {
setImageError(result.error || 'Failed to save image');
}
} catch (err) {
setImageError('Failed to process pasted image');
}
}
}, [selectedProjectId, generatedChangelog, onChangelogEdit]);
// Handle drag and drop
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
}, []);
const handleDragLeave = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDrop = useCallback(async (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (!selectedProjectId) return;
const files = Array.from(e.dataTransfer.files);
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
if (imageFiles.length === 0) return;
setImageError(null);
for (const file of imageFiles) {
if (!isValidImageMimeType(file.type)) {
setImageError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`);
continue;
}
try {
const dataUrl = await blobToBase64(file);
const extension = file.name.split('.').pop() || file.type.split('/')[1] || 'png';
const timestamp = Date.now();
const baseFilename = `changelog-${timestamp}.${extension}`;
const filename = resolveFilename(baseFilename, []);
const result = await window.electronAPI.saveChangelogImage(
selectedProjectId,
dataUrl,
filename
);
if (result.success && result.data) {
// Insert markdown image at cursor position or end
const textarea = textareaRef.current;
if (textarea) {
const cursorPos = textarea.selectionStart;
const textBefore = generatedChangelog.substring(0, cursorPos);
const textAfter = generatedChangelog.substring(cursorPos);
const imageMarkdown = `\n![${filename}](${result.data.relativePath})\n`;
onChangelogEdit(textBefore + imageMarkdown + textAfter);
// Set cursor position after inserted image
setTimeout(() => {
const newPos = cursorPos + imageMarkdown.length;
textarea.setSelectionRange(newPos, newPos);
textarea.focus();
}, 0);
}
} else {
setImageError(result.error || 'Failed to save image');
}
} catch (err) {
setImageError('Failed to process dropped image');
}
}
}, [selectedProjectId, generatedChangelog, onChangelogEdit]);
// Get summary info based on source mode
const getSummaryInfo = () => {
@@ -240,6 +386,30 @@ export function Step2ConfigureGenerate({
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Emojis</Label>
<Select
value={emojiLevel}
onValueChange={(value) => onEmojiLevelChange(value as ChangelogEmojiLevel)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(CHANGELOG_EMOJI_LEVEL_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
<div>
<div>{label}</div>
<div className="text-xs text-muted-foreground">
{CHANGELOG_EMOJI_LEVEL_DESCRIPTIONS[value]}
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
@@ -367,14 +537,34 @@ export function Step2ConfigureGenerate({
</div>
{/* Preview Content */}
<div className="flex-1 overflow-hidden p-6">
<div
className={`flex-1 overflow-hidden p-6 ${isDragOver ? 'bg-muted/50' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{generatedChangelog ? (
<Textarea
className="h-full w-full resize-none font-mono text-sm"
value={generatedChangelog}
onChange={(e) => onChangelogEdit(e.target.value)}
placeholder="Generated changelog will appear here..."
/>
<>
{isDragOver && (
<div className="mb-4 rounded-lg border-2 border-dashed border-primary/50 bg-primary/5 p-4 text-center">
<ImageIcon className="mx-auto h-8 w-8 text-primary/50" />
<p className="mt-2 text-sm text-primary/70">Drop images here to add to changelog</p>
</div>
)}
{imageError && (
<div className="mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
{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={handlePaste}
placeholder="Generated changelog will appear here... (Drag & drop or paste images to add)"
/>
</>
) : (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@@ -382,6 +572,9 @@ export function Step2ConfigureGenerate({
<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>
</div>
</div>
)}
@@ -52,6 +52,7 @@ export function useChangelog() {
const date = useChangelogStore((state) => state.date);
const format = useChangelogStore((state) => state.format);
const audience = useChangelogStore((state) => state.audience);
const emojiLevel = useChangelogStore((state) => state.emojiLevel);
const customInstructions = useChangelogStore((state) => state.customInstructions);
const generationProgress = useChangelogStore((state) => state.generationProgress);
const generatedChangelog = useChangelogStore((state) => state.generatedChangelog);
@@ -84,6 +85,7 @@ export function useChangelog() {
const setDate = useChangelogStore((state) => state.setDate);
const setFormat = useChangelogStore((state) => state.setFormat);
const setAudience = useChangelogStore((state) => state.setAudience);
const setEmojiLevel = useChangelogStore((state) => state.setEmojiLevel);
const setCustomInstructions = useChangelogStore((state) => state.setCustomInstructions);
const updateGeneratedChangelog = useChangelogStore((state) => state.updateGeneratedChangelog);
const setError = useChangelogStore((state) => state.setError);
@@ -275,6 +277,7 @@ export function useChangelog() {
date,
format,
audience,
emojiLevel,
customInstructions,
generationProgress,
generatedChangelog,
@@ -306,6 +309,7 @@ export function useChangelog() {
setDate,
setFormat,
setAudience,
setEmojiLevel,
setCustomInstructions,
updateGeneratedChangelog,
setShowAdvanced,
@@ -4,6 +4,7 @@ import type {
TaskSpecContent,
ChangelogFormat,
ChangelogAudience,
ChangelogEmojiLevel,
ChangelogGenerationProgress,
ChangelogGenerationResult,
ExistingChangelog,
@@ -54,6 +55,7 @@ interface ChangelogState {
date: string;
format: ChangelogFormat;
audience: ChangelogAudience;
emojiLevel: ChangelogEmojiLevel;
customInstructions: string;
// Generation state
@@ -101,6 +103,7 @@ interface ChangelogState {
setDate: (date: string) => void;
setFormat: (format: ChangelogFormat) => void;
setAudience: (audience: ChangelogAudience) => void;
setEmojiLevel: (level: ChangelogEmojiLevel) => void;
setCustomInstructions: (instructions: string) => void;
// Generation actions
@@ -154,6 +157,7 @@ const initialState = {
date: getDefaultDate(),
format: 'keep-a-changelog' as ChangelogFormat,
audience: 'user-facing' as ChangelogAudience,
emojiLevel: 'none' as ChangelogEmojiLevel,
customInstructions: '',
generationProgress: null as ChangelogGenerationProgress | null,
@@ -237,6 +241,7 @@ export const useChangelogStore = create<ChangelogState>((set, get) => ({
setDate: (date) => set({ date }),
setFormat: (format) => set({ format }),
setAudience: (audience) => set({ audience }),
setEmojiLevel: (level) => set({ emojiLevel: level }),
setCustomInstructions: (instructions) => set({ customInstructions: instructions }),
// Generation actions
@@ -444,6 +449,7 @@ export function generateChangelog(projectId: string): void {
date: store.date,
format: store.format,
audience: store.audience,
emojiLevel: store.emojiLevel !== 'none' ? store.emojiLevel : undefined,
customInstructions: store.customInstructions || undefined
};
@@ -15,6 +15,7 @@ interface RoadmapState {
setRoadmap: (roadmap: Roadmap | null) => void;
setGenerationStatus: (status: RoadmapGenerationStatus) => void;
updateFeatureStatus: (featureId: string, status: RoadmapFeatureStatus) => void;
updateFeatureLinkedSpec: (featureId: string, specId: string) => void;
clearRoadmap: () => void;
}
@@ -51,6 +52,25 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
};
}),
updateFeatureLinkedSpec: (featureId, specId) =>
set((state) => {
if (!state.roadmap) return state;
const updatedFeatures = state.roadmap.features.map((feature) =>
feature.id === featureId
? { ...feature, linkedSpecId: specId, status: 'planned' as RoadmapFeatureStatus }
: feature
);
return {
roadmap: {
...state.roadmap,
features: updatedFeatures,
updatedAt: new Date()
}
};
}),
clearRoadmap: () =>
set({
roadmap: null,
+17 -1
View File
@@ -318,6 +318,7 @@ export const IPC_CHANNELS = {
CHANGELOG_GET_BRANCHES: 'changelog:getBranches',
CHANGELOG_GET_TAGS: 'changelog:getTags',
CHANGELOG_GET_COMMITS_PREVIEW: 'changelog:getCommitsPreview',
CHANGELOG_SAVE_IMAGE: 'changelog:saveImage',
// Changelog events (main -> renderer)
CHANGELOG_GENERATION_PROGRESS: 'changelog:generationProgress',
@@ -685,7 +686,7 @@ export const CHANGELOG_FORMAT_LABELS: Record<string, string> = {
export const CHANGELOG_FORMAT_DESCRIPTIONS: Record<string, string> = {
'keep-a-changelog': 'Structured format with Added/Changed/Fixed/Removed sections',
'simple-list': 'Clean bulleted list with categories',
'github-release': 'GitHub-style release notes with emojis'
'github-release': 'GitHub-style release notes'
};
// Changelog audience labels and descriptions
@@ -701,6 +702,21 @@ export const CHANGELOG_AUDIENCE_DESCRIPTIONS: Record<string, string> = {
'marketing': 'Value-focused copy emphasizing benefits'
};
// Changelog emoji level labels and descriptions
export const CHANGELOG_EMOJI_LEVEL_LABELS: Record<string, string> = {
'none': 'None',
'little': 'Headings Only',
'medium': 'Headings + Highlights',
'high': 'Everything'
};
export const CHANGELOG_EMOJI_LEVEL_DESCRIPTIONS: Record<string, string> = {
'none': 'No emojis',
'little': 'Emojis on section headings only',
'medium': 'Emojis on headings and key items',
'high': 'Emojis on headings and every line'
};
// Changelog generation stage labels
export const CHANGELOG_STAGE_LABELS: Record<string, string> = {
'loading_specs': 'Loading spec files...',
@@ -10,6 +10,7 @@ import type { ImplementationPlan } from './task';
export type ChangelogFormat = 'keep-a-changelog' | 'simple-list' | 'github-release';
export type ChangelogAudience = 'technical' | 'user-facing' | 'marketing';
export type ChangelogEmojiLevel = 'none' | 'little' | 'medium' | 'high';
export interface ChangelogTask {
id: string;
@@ -95,6 +96,7 @@ export interface ChangelogGenerationRequest {
date: string; // ISO format
format: ChangelogFormat;
audience: ChangelogAudience;
emojiLevel?: ChangelogEmojiLevel; // Optional emoji usage level
customInstructions?: string;
}
+5
View File
@@ -377,6 +377,11 @@ export interface ElectronAPI {
options: GitHistoryOptions | BranchDiffOptions,
mode: 'git-history' | 'branch-diff'
) => Promise<IPCResult<GitCommit[]>>;
saveChangelogImage: (
projectId: string,
imageData: string,
filename: string
) => Promise<IPCResult<{ relativePath: string; url: string }>>;
// Changelog event listeners
onChangelogGenerationProgress: (