# CHANGES

- Updated app icons with fresh new design aesthetic! 🎨
- Added markdown rendering support for group chat messages 📝
- Implemented toggle between formatted and plain text views 👁️
- Added copy-to-clipboard functionality for chat messages 📋
- Improved phase generation logic for better document detection 🔍
- Enhanced disk document validation with task counting checks 
- Fixed Claude Code file output handling in wizard mode 🛠️
- Added keyboard shortcut (⌘E) for markdown view toggle ⌨️
- Improved UI with hover effects on message action buttons 
- Better handling of documents written directly to disk 💾
This commit is contained in:
Pedram Amini
2025-12-18 17:28:11 -06:00
parent d9ebe1c9a4
commit 9d5b0306a4
7 changed files with 83 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -5567,6 +5567,8 @@ export default function MaestroConsole() {
executionQueue={groupChatExecutionQueue}
onRemoveQueuedItem={handleRemoveGroupChatQueueItem}
onReorderQueuedItems={handleReorderGroupChatQueueItems}
markdownEditMode={markdownEditMode}
onToggleMarkdownEditMode={() => setMarkdownEditMode(!markdownEditMode)}
/>
</div>
<GroupChatParticipants

View File

@@ -6,15 +6,19 @@
* align left with colored borders. Includes auto-scroll and typing indicator.
*/
import { useRef, useEffect } from 'react';
import { BookOpen } from 'lucide-react';
import { useRef, useEffect, useCallback } from 'react';
import { BookOpen, Eye, FileText, Copy } from 'lucide-react';
import type { GroupChatMessage, GroupChatParticipant, GroupChatState, Theme } from '../types';
import { MarkdownRenderer } from './MarkdownRenderer';
import { stripMarkdown } from '../utils/textProcessing';
interface GroupChatMessagesProps {
theme: Theme;
messages: GroupChatMessage[];
participants: GroupChatParticipant[];
state: GroupChatState;
markdownEditMode?: boolean;
onToggleMarkdownEditMode?: () => void;
}
// Color mapping for participants
@@ -32,9 +36,15 @@ export function GroupChatMessages({
messages,
participants,
state,
markdownEditMode,
onToggleMarkdownEditMode,
}: GroupChatMessagesProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const copyToClipboard = useCallback((text: string) => {
navigator.clipboard.writeText(text);
}, []);
// Auto-scroll on new messages
useEffect(() => {
if (containerRef.current) {
@@ -102,7 +112,7 @@ export function GroupChatMessages({
className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}
>
<div
className="max-w-[70%] rounded-lg px-4 py-2"
className="group relative max-w-[70%] rounded-lg px-4 py-2"
style={{
backgroundColor: style.bgColor,
color: style.textColor,
@@ -120,9 +130,19 @@ export function GroupChatMessages({
)}
{/* Message content */}
<div className="text-sm whitespace-pre-wrap">
{msg.content}
</div>
{!isUser && !markdownEditMode ? (
<div className="text-sm">
<MarkdownRenderer
content={msg.content}
theme={theme}
onCopy={copyToClipboard}
/>
</div>
) : (
<div className="text-sm whitespace-pre-wrap">
{isUser ? msg.content : stripMarkdown(msg.content)}
</div>
)}
{/* Timestamp and read-only indicator */}
<div className="flex items-center gap-2 mt-1">
@@ -143,6 +163,35 @@ export function GroupChatMessages({
</span>
)}
</div>
{/* Action buttons - bottom right corner (non-user messages only) */}
{!isUser && (
<div
className="absolute bottom-2 right-2 flex items-center gap-1"
style={{ transition: 'opacity 0.15s ease-in-out' }}
>
{/* Markdown toggle button */}
{onToggleMarkdownEditMode && (
<button
onClick={onToggleMarkdownEditMode}
className="p-1.5 rounded opacity-0 group-hover:opacity-50 hover:!opacity-100"
style={{ color: markdownEditMode ? theme.colors.accent : theme.colors.textDim }}
title={markdownEditMode ? "Show formatted (⌘E)" : "Show plain text (⌘E)"}
>
{markdownEditMode ? <Eye className="w-4 h-4" /> : <FileText className="w-4 h-4" />}
</button>
)}
{/* Copy to Clipboard Button */}
<button
onClick={() => copyToClipboard(msg.content)}
className="p-1.5 rounded opacity-0 group-hover:opacity-50 hover:!opacity-100"
style={{ color: theme.colors.textDim }}
title="Copy to clipboard"
>
<Copy className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
</div>
);

View File

@@ -41,6 +41,9 @@ interface GroupChatPanelProps {
executionQueue?: QueuedItem[];
onRemoveQueuedItem?: (itemId: string) => void;
onReorderQueuedItems?: (fromIndex: number, toIndex: number) => void;
// Markdown toggle (Cmd+E)
markdownEditMode?: boolean;
onToggleMarkdownEditMode?: () => void;
}
export function GroupChatPanel({
@@ -68,6 +71,8 @@ export function GroupChatPanel({
executionQueue,
onRemoveQueuedItem,
onReorderQueuedItems,
markdownEditMode,
onToggleMarkdownEditMode,
}: GroupChatPanelProps): JSX.Element {
return (
<div
@@ -91,6 +96,8 @@ export function GroupChatPanel({
messages={messages}
participants={groupChat.participants}
state={state}
markdownEditMode={markdownEditMode}
onToggleMarkdownEditMode={onToggleMarkdownEditMode}
/>
<GroupChatInput

View File

@@ -436,6 +436,7 @@ class PhaseGenerator {
const rawOutput = result.rawOutput || '';
let documents = parseGeneratedDocuments(rawOutput);
let documentsFromDisk = false;
// If no documents parsed with markers, try splitting intelligently
if (documents.length === 0 && rawOutput.trim()) {
@@ -443,16 +444,28 @@ class PhaseGenerator {
documents = splitIntoPhases(rawOutput);
}
// If still no documents, check if files were written directly to disk
// (Claude Code may write files directly instead of outputting with markers)
let documentsFromDisk = false;
if (documents.length === 0) {
// Validate that parsed documents contain actual tasks
// If the agent wrote files directly to disk (Claude Code's normal behavior),
// the rawOutput won't contain document content, just status messages.
// splitIntoPhases would create a single document from that status text,
// which wouldn't contain any valid tasks.
const totalTasksFromParsed = documents.reduce((sum, doc) => sum + countTasks(doc.content), 0);
const hasValidParsedDocs = documents.length > 0 && totalTasksFromParsed > 0;
// Check for files on disk if:
// 1. No documents were parsed at all, OR
// 2. Parsed documents don't contain valid tasks (likely just status output)
if (!hasValidParsedDocs) {
callbacks?.onProgress?.('Checking for documents on disk...');
const diskDocs = await this.readDocumentsFromDisk(config.directoryPath);
if (diskDocs.length > 0) {
console.log('[PhaseGenerator] Found documents on disk:', diskDocs.length);
documents = diskDocs;
documentsFromDisk = true;
// Prefer disk documents if they have more content/tasks
const totalTasksFromDisk = diskDocs.reduce((sum, doc) => sum + countTasks(doc.content), 0);
if (totalTasksFromDisk >= totalTasksFromParsed) {
documents = diskDocs;
documentsFromDisk = true;
}
}
}