Version 2.0.1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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\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\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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: (
|
||||
|
||||
Reference in New Issue
Block a user