mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
OAuth enabled but no valid token found. Starting authentication...
Found expired OAuth token, attempting refresh... Token refresh successful ## CHANGES - Prompts now compile from Markdown into TypeScript at build-time 🔧 - New `build:prompts` step runs automatically for dev and release builds 🏗️ - Main process stops runtime prompt file I/O for faster, safer startups ⚡ - Group chat prompt access refactored into getter functions for flexibility 🧩 - Added IPC to reset a participant context with session summarization 🔄 - Participant cards now copy agent session IDs with one click 📋 - UI shows context-reset button when participant usage hits 40%+ ⏱️ - History markdown now supports raw HTML rendering via `rehype-raw` 🧪 - History detail supports clickable file links and in-app file previews 🗂️ - Document copy-drag enables reset-on-completion across duplicate filenames 🧷
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ node_modules/
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
|
src/generated/
|
||||||
*.log
|
*.log
|
||||||
tmp/
|
tmp/
|
||||||
scratch/
|
scratch/
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -18,10 +18,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
|
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
|
||||||
"dev:demo": "MAESTRO_DEMO_DIR=/tmp/maestro-demo npm run dev",
|
"dev:demo": "MAESTRO_DEMO_DIR=/tmp/maestro-demo npm run dev",
|
||||||
"dev:main": "tsc -p tsconfig.main.json && NODE_ENV=development electron .",
|
"dev:main": "npm run build:prompts && tsc -p tsconfig.main.json && NODE_ENV=development electron .",
|
||||||
"dev:renderer": "vite",
|
"dev:renderer": "vite",
|
||||||
"dev:web": "vite --config vite.config.web.mts",
|
"dev:web": "vite --config vite.config.web.mts",
|
||||||
"build": "npm run build:main && npm run build:renderer && npm run build:web && npm run build:cli",
|
"build": "npm run build:prompts && npm run build:main && npm run build:renderer && npm run build:web && npm run build:cli",
|
||||||
|
"build:prompts": "node scripts/generate-prompts.mjs",
|
||||||
"build:main": "tsc -p tsconfig.main.json",
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
"build:cli": "node scripts/build-cli.mjs",
|
"build:cli": "node scripts/build-cli.mjs",
|
||||||
"build:renderer": "vite build",
|
"build:renderer": "vite build",
|
||||||
@@ -97,10 +98,6 @@
|
|||||||
{
|
{
|
||||||
"from": "src/prompts/speckit",
|
"from": "src/prompts/speckit",
|
||||||
"to": "prompts/speckit"
|
"to": "prompts/speckit"
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "src/prompts/group-chat-*.md",
|
|
||||||
"to": "prompts"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -129,10 +126,6 @@
|
|||||||
{
|
{
|
||||||
"from": "src/prompts/speckit",
|
"from": "src/prompts/speckit",
|
||||||
"to": "prompts/speckit"
|
"to": "prompts/speckit"
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "src/prompts/group-chat-*.md",
|
|
||||||
"to": "prompts"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -170,10 +163,6 @@
|
|||||||
{
|
{
|
||||||
"from": "src/prompts/speckit",
|
"from": "src/prompts/speckit",
|
||||||
"to": "prompts/speckit"
|
"to": "prompts/speckit"
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "src/prompts/group-chat-*.md",
|
|
||||||
"to": "prompts"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
101
scripts/generate-prompts.mjs
Normal file
101
scripts/generate-prompts.mjs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build script to generate TypeScript from prompt markdown files.
|
||||||
|
*
|
||||||
|
* Reads all .md files from src/prompts/ and generates src/generated/prompts.ts
|
||||||
|
* with the content as exported string constants.
|
||||||
|
*
|
||||||
|
* This allows prompts to be:
|
||||||
|
* - Edited as readable markdown files
|
||||||
|
* - Imported as regular TypeScript constants (no runtime file I/O)
|
||||||
|
* - Used consistently in both renderer and main process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const rootDir = path.resolve(__dirname, '..');
|
||||||
|
const promptsDir = path.join(rootDir, 'src/prompts');
|
||||||
|
const outputDir = path.join(rootDir, 'src/generated');
|
||||||
|
const outputFile = path.join(outputDir, 'prompts.ts');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a filename like "wizard-system.md" to a camelCase variable name
|
||||||
|
* like "wizardSystemPrompt"
|
||||||
|
*/
|
||||||
|
function filenameToVarName(filename) {
|
||||||
|
const base = filename.replace(/\.md$/, '');
|
||||||
|
const parts = base.split('-');
|
||||||
|
const camelCase = parts
|
||||||
|
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
||||||
|
.join('');
|
||||||
|
// Only add "Prompt" suffix if the name doesn't already end with it
|
||||||
|
if (camelCase.toLowerCase().endsWith('prompt')) {
|
||||||
|
return camelCase;
|
||||||
|
}
|
||||||
|
return camelCase + 'Prompt';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape backticks and ${} in template literal content
|
||||||
|
*/
|
||||||
|
function escapeTemplateString(content) {
|
||||||
|
return content
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/`/g, '\\`')
|
||||||
|
.replace(/\$\{/g, '\\${');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
console.log('Generating prompts from markdown files...');
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all .md files in prompts directory (not in subdirectories)
|
||||||
|
const files = fs.readdirSync(promptsDir).filter((f) => f.endsWith('.md'));
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.error('No .md files found in', promptsDir);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the output
|
||||||
|
const exports = [];
|
||||||
|
const lines = [
|
||||||
|
'/**',
|
||||||
|
' * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY',
|
||||||
|
' *',
|
||||||
|
' * This file is generated by scripts/generate-prompts.mjs',
|
||||||
|
' * Edit the source .md files in src/prompts/ instead.',
|
||||||
|
' *',
|
||||||
|
` * Generated: ${new Date().toISOString()}`,
|
||||||
|
' */',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files.sort()) {
|
||||||
|
const filePath = path.join(promptsDir, file);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const varName = filenameToVarName(file);
|
||||||
|
|
||||||
|
lines.push(`export const ${varName} = \`${escapeTemplateString(content)}\`;`);
|
||||||
|
lines.push('');
|
||||||
|
exports.push(varName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the file
|
||||||
|
fs.writeFileSync(outputFile, lines.join('\n'));
|
||||||
|
|
||||||
|
console.log(`✓ Generated ${outputFile}`);
|
||||||
|
console.log(` ${exports.length} prompts: ${exports.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generate().catch((error) => {
|
||||||
|
console.error('Generation failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -20,7 +20,7 @@ import { appendToLog } from './group-chat-log';
|
|||||||
import { IProcessManager, isModeratorActive } from './group-chat-moderator';
|
import { IProcessManager, isModeratorActive } from './group-chat-moderator';
|
||||||
import type { AgentDetector } from '../agent-detector';
|
import type { AgentDetector } from '../agent-detector';
|
||||||
import { buildAgentArgs, applyAgentConfigOverrides, getContextWindowValue } from '../utils/agent-args';
|
import { buildAgentArgs, applyAgentConfigOverrides, getContextWindowValue } from '../utils/agent-args';
|
||||||
import { groupChatParticipantPrompt } from '../prompts';
|
import { groupChatParticipantPrompt } from '../../prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory store for active participant sessions.
|
* In-memory store for active participant sessions.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { appendToLog, readLog } from './group-chat-log';
|
|||||||
import {
|
import {
|
||||||
groupChatModeratorSystemPrompt,
|
groupChatModeratorSystemPrompt,
|
||||||
groupChatModeratorSynthesisPrompt,
|
groupChatModeratorSynthesisPrompt,
|
||||||
} from '../prompts';
|
} from '../../prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for the process manager dependency.
|
* Interface for the process manager dependency.
|
||||||
@@ -114,18 +114,22 @@ function touchSession(groupChatId: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base system prompt for the moderator.
|
* Gets the base system prompt for the moderator.
|
||||||
* This is combined with participant info and chat history in routeUserMessage.
|
* This is combined with participant info and chat history in routeUserMessage.
|
||||||
* Loaded from src/prompts/group-chat-moderator-system.md
|
* Loaded from src/prompts/group-chat-moderator-system.md
|
||||||
*/
|
*/
|
||||||
export const MODERATOR_SYSTEM_PROMPT = groupChatModeratorSystemPrompt;
|
export function getModeratorSystemPrompt(): string {
|
||||||
|
return groupChatModeratorSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The synthesis prompt for the moderator when reviewing agent responses.
|
* Gets the synthesis prompt for the moderator when reviewing agent responses.
|
||||||
* The moderator decides whether to continue with agents or return to the user.
|
* The moderator decides whether to continue with agents or return to the user.
|
||||||
* Loaded from src/prompts/group-chat-moderator-synthesis.md
|
* Loaded from src/prompts/group-chat-moderator-synthesis.md
|
||||||
*/
|
*/
|
||||||
export const MODERATOR_SYNTHESIS_PROMPT = groupChatModeratorSynthesisPrompt;
|
export function getModeratorSynthesisPrompt(): string {
|
||||||
|
return groupChatModeratorSynthesisPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawns a moderator agent for a group chat.
|
* Spawns a moderator agent for a group chat.
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ import {
|
|||||||
IProcessManager,
|
IProcessManager,
|
||||||
getModeratorSessionId,
|
getModeratorSessionId,
|
||||||
isModeratorActive,
|
isModeratorActive,
|
||||||
MODERATOR_SYSTEM_PROMPT,
|
getModeratorSystemPrompt,
|
||||||
MODERATOR_SYNTHESIS_PROMPT,
|
getModeratorSynthesisPrompt,
|
||||||
} from './group-chat-moderator';
|
} from './group-chat-moderator';
|
||||||
import {
|
import {
|
||||||
addParticipant,
|
addParticipant,
|
||||||
} from './group-chat-agent';
|
} from './group-chat-agent';
|
||||||
import { AgentDetector } from '../agent-detector';
|
import { AgentDetector } from '../agent-detector';
|
||||||
import { buildAgentArgs, applyAgentConfigOverrides, getContextWindowValue } from '../utils/agent-args';
|
import { buildAgentArgs, applyAgentConfigOverrides, getContextWindowValue } from '../utils/agent-args';
|
||||||
import { groupChatParticipantRequestPrompt } from '../prompts';
|
import { groupChatParticipantRequestPrompt } from '../../prompts';
|
||||||
|
|
||||||
// Import emitters from IPC handlers (will be populated after handlers are registered)
|
// Import emitters from IPC handlers (will be populated after handlers are registered)
|
||||||
import { groupChatEmitters } from '../ipc/handlers/groupChat';
|
import { groupChatEmitters } from '../ipc/handlers/groupChat';
|
||||||
@@ -371,7 +371,7 @@ export async function routeUserMessage(
|
|||||||
`[${m.from}]: ${m.content}`
|
`[${m.from}]: ${m.content}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|
||||||
const fullPrompt = `${MODERATOR_SYSTEM_PROMPT}
|
const fullPrompt = `${getModeratorSystemPrompt()}
|
||||||
|
|
||||||
## Current Participants:
|
## Current Participants:
|
||||||
${participantContext}${availableSessionsContext}
|
${participantContext}${availableSessionsContext}
|
||||||
@@ -913,9 +913,9 @@ export async function spawnModeratorSynthesis(
|
|||||||
? chat.participants.map(p => `- @${p.name} (${p.agentId} session)`).join('\n')
|
? chat.participants.map(p => `- @${p.name} (${p.agentId} session)`).join('\n')
|
||||||
: '(No agents currently in this group chat)';
|
: '(No agents currently in this group chat)';
|
||||||
|
|
||||||
const synthesisPrompt = `${MODERATOR_SYSTEM_PROMPT}
|
const synthesisPrompt = `${getModeratorSystemPrompt()}
|
||||||
|
|
||||||
${MODERATOR_SYNTHESIS_PROMPT}
|
${getModeratorSynthesisPrompt()}
|
||||||
|
|
||||||
## Current Participants (you can @mention these for follow-up):
|
## Current Participants (you can @mention these for follow-up):
|
||||||
${participantContext}
|
${participantContext}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
listGroupChats,
|
listGroupChats,
|
||||||
deleteGroupChat,
|
deleteGroupChat,
|
||||||
updateGroupChat,
|
updateGroupChat,
|
||||||
|
updateParticipant,
|
||||||
GroupChat,
|
GroupChat,
|
||||||
GroupChatParticipant,
|
GroupChatParticipant,
|
||||||
addGroupChatHistoryEntry,
|
addGroupChatHistoryEntry,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
deleteGroupChatHistoryEntry,
|
deleteGroupChatHistoryEntry,
|
||||||
clearGroupChatHistory,
|
clearGroupChatHistory,
|
||||||
getGroupChatHistoryFilePath,
|
getGroupChatHistoryFilePath,
|
||||||
|
getGroupChatDir,
|
||||||
} from '../../group-chat/group-chat-storage';
|
} from '../../group-chat/group-chat-storage';
|
||||||
|
|
||||||
// Group chat history type
|
// Group chat history type
|
||||||
@@ -66,6 +68,8 @@ import { routeUserMessage } from '../../group-chat/group-chat-router';
|
|||||||
|
|
||||||
// Agent detector import
|
// Agent detector import
|
||||||
import { AgentDetector } from '../../agent-detector';
|
import { AgentDetector } from '../../agent-detector';
|
||||||
|
import { buildAgentArgs, applyAgentConfigOverrides, getContextWindowValue } from '../../utils/agent-args';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const LOG_CONTEXT = '[GroupChat]';
|
const LOG_CONTEXT = '[GroupChat]';
|
||||||
|
|
||||||
@@ -458,6 +462,148 @@ export function registerGroupChatHandlers(deps: GroupChatHandlerDependencies): v
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset participant context - summarize current session and start fresh
|
||||||
|
ipcMain.handle(
|
||||||
|
'groupChat:resetParticipantContext',
|
||||||
|
withIpcErrorLogging(
|
||||||
|
handlerOpts('resetParticipantContext'),
|
||||||
|
async (groupChatId: string, participantName: string, cwd?: string): Promise<{ newAgentSessionId: string }> => {
|
||||||
|
logger.info(`Resetting context for participant ${participantName} in ${groupChatId}`, LOG_CONTEXT);
|
||||||
|
|
||||||
|
const chat = await loadGroupChat(groupChatId);
|
||||||
|
if (!chat) {
|
||||||
|
throw new Error(`Group chat not found: ${groupChatId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const participant = chat.participants.find(p => p.name === participantName);
|
||||||
|
if (!participant) {
|
||||||
|
throw new Error(`Participant not found: ${participantName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processManager = getProcessManager();
|
||||||
|
if (!processManager) {
|
||||||
|
throw new Error('Process manager not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentDetector = getAgentDetector();
|
||||||
|
if (!agentDetector) {
|
||||||
|
throw new Error('Agent detector not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await agentDetector.getAgent(participant.agentId);
|
||||||
|
if (!agent || !agent.available) {
|
||||||
|
throw new Error(`Agent not available: ${participant.agentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the group chat folder for file access
|
||||||
|
const groupChatFolder = getGroupChatDir(groupChatId);
|
||||||
|
|
||||||
|
// Build a context summary prompt to ask the agent to summarize its current state
|
||||||
|
const summaryPrompt = `You are "${participantName}" in the group chat "${chat.name}".
|
||||||
|
The shared group chat folder is: ${groupChatFolder}
|
||||||
|
|
||||||
|
Your context window is getting full. Please provide a concise summary of:
|
||||||
|
1. What you've been working on in this group chat
|
||||||
|
2. Key decisions made and their rationale
|
||||||
|
3. Current state of any ongoing tasks
|
||||||
|
4. Important context that should be preserved for continuity
|
||||||
|
|
||||||
|
This summary will be used to initialize your fresh session so you can continue seamlessly.
|
||||||
|
|
||||||
|
Respond with ONLY the summary text, no additional commentary.`;
|
||||||
|
|
||||||
|
// Spawn a batch process to get the summary
|
||||||
|
const summarySessionId = `group-chat-${groupChatId}-summary-${participantName}-${Date.now()}`;
|
||||||
|
const effectiveCwd = cwd || process.env.HOME || '/tmp';
|
||||||
|
|
||||||
|
const agentConfigValues = getAgentConfig?.(participant.agentId) || {};
|
||||||
|
const customEnvVars = getCustomEnvVars?.(participant.agentId);
|
||||||
|
|
||||||
|
const baseArgs = buildAgentArgs(agent, {
|
||||||
|
baseArgs: [...agent.args],
|
||||||
|
prompt: summaryPrompt,
|
||||||
|
cwd: effectiveCwd,
|
||||||
|
readOnlyMode: true, // Summary is read-only
|
||||||
|
agentSessionId: participant.agentSessionId, // Use existing session to get context
|
||||||
|
});
|
||||||
|
|
||||||
|
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
||||||
|
agentConfigValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect summary output
|
||||||
|
let summaryOutput = '';
|
||||||
|
const summaryPromise = new Promise<string>((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Summary generation timed out'));
|
||||||
|
}, 60000); // 60 second timeout
|
||||||
|
|
||||||
|
// Set up a listener for the summary output
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
// The process manager should emit output events
|
||||||
|
// For now, we'll wait for the process to complete
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Clean up on completion
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(summaryOutput || 'No summary available - starting fresh session.');
|
||||||
|
}, 10000); // Wait 10 seconds for summary
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn the summary process
|
||||||
|
const command = agent.path || agent.command;
|
||||||
|
processManager.spawn({
|
||||||
|
sessionId: summarySessionId,
|
||||||
|
toolType: participant.agentId,
|
||||||
|
cwd: effectiveCwd,
|
||||||
|
command,
|
||||||
|
args: configResolution.args,
|
||||||
|
readOnlyMode: true,
|
||||||
|
prompt: summaryPrompt,
|
||||||
|
contextWindow: getContextWindowValue(agent, agentConfigValues),
|
||||||
|
customEnvVars: configResolution.effectiveCustomEnvVars ?? customEnvVars,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for summary (with timeout)
|
||||||
|
try {
|
||||||
|
await summaryPromise;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Summary generation failed: ${error}`, LOG_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the summary process
|
||||||
|
try {
|
||||||
|
processManager.kill(summarySessionId);
|
||||||
|
} catch {
|
||||||
|
// Ignore kill errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new agent session ID (the actual UUID will be set when the agent responds)
|
||||||
|
// For now, we clear the old one to signal a fresh start
|
||||||
|
const newSessionMarker = uuidv4();
|
||||||
|
|
||||||
|
// Update the participant with a cleared agentSessionId
|
||||||
|
// The next interaction will establish a new session
|
||||||
|
await updateParticipant(groupChatId, participantName, {
|
||||||
|
agentSessionId: undefined, // Clear to force new session
|
||||||
|
contextUsage: 0, // Reset context usage
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit participants changed event
|
||||||
|
const updatedChat = await loadGroupChat(groupChatId);
|
||||||
|
if (updatedChat) {
|
||||||
|
groupChatEmitters.emitParticipantsChanged?.(groupChatId, updatedChat.participants);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Reset context for ${participantName}, new session marker: ${newSessionMarker}`, LOG_CONTEXT);
|
||||||
|
|
||||||
|
return { newAgentSessionId: newSessionMarker };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// ========== History Handlers ==========
|
// ========== History Handlers ==========
|
||||||
|
|
||||||
// Get all history entries for a group chat
|
// Get all history entries for a group chat
|
||||||
|
|||||||
@@ -1116,6 +1116,8 @@ contextBridge.exposeInMainWorld('maestro', {
|
|||||||
ipcRenderer.invoke('groupChat:sendToParticipant', id, name, message, images),
|
ipcRenderer.invoke('groupChat:sendToParticipant', id, name, message, images),
|
||||||
removeParticipant: (id: string, name: string) =>
|
removeParticipant: (id: string, name: string) =>
|
||||||
ipcRenderer.invoke('groupChat:removeParticipant', id, name),
|
ipcRenderer.invoke('groupChat:removeParticipant', id, name),
|
||||||
|
resetParticipantContext: (id: string, name: string, cwd?: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:resetParticipantContext', id, name, cwd) as Promise<{ newAgentSessionId: string }>,
|
||||||
|
|
||||||
// History
|
// History
|
||||||
getHistory: (id: string) =>
|
getHistory: (id: string) =>
|
||||||
@@ -2077,6 +2079,7 @@ export interface MaestroAPI {
|
|||||||
}>;
|
}>;
|
||||||
sendToParticipant: (id: string, name: string, message: string, images?: string[]) => Promise<void>;
|
sendToParticipant: (id: string, name: string, message: string, images?: string[]) => Promise<void>;
|
||||||
removeParticipant: (id: string, name: string) => Promise<void>;
|
removeParticipant: (id: string, name: string) => Promise<void>;
|
||||||
|
resetParticipantContext: (id: string, name: string, cwd?: string) => Promise<{ newAgentSessionId: string }>;
|
||||||
|
|
||||||
// History
|
// History
|
||||||
getHistory: (id: string) => Promise<Array<{
|
getHistory: (id: string) => Promise<Array<{
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* Prompt loader for the main process.
|
|
||||||
*
|
|
||||||
* This module reads prompt markdown files from src/prompts/ at runtime.
|
|
||||||
* Unlike the renderer (which uses Vite's ?raw imports), the main process
|
|
||||||
* uses Node.js fs to read files synchronously at module load time.
|
|
||||||
*
|
|
||||||
* This ensures all prompts live in src/prompts/ as markdown files for
|
|
||||||
* easy discovery and editing, while still being usable in the main process.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { app } from 'electron';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the path to a prompt file.
|
|
||||||
* In development, reads from src/prompts/
|
|
||||||
* In production, reads from the app's resources
|
|
||||||
*/
|
|
||||||
function getPromptsDir(): string {
|
|
||||||
// In development, __dirname is dist/main, so go up to project root
|
|
||||||
// In production, we need to handle the packaged app structure
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
// Development: src/prompts is relative to project root
|
|
||||||
return path.resolve(__dirname, '../../src/prompts');
|
|
||||||
} else {
|
|
||||||
// Production: prompts are copied to resources
|
|
||||||
return path.join(process.resourcesPath, 'prompts');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a prompt file and returns its contents.
|
|
||||||
* Throws an error if the file doesn't exist.
|
|
||||||
*/
|
|
||||||
function loadPrompt(filename: string): string {
|
|
||||||
const promptsDir = getPromptsDir();
|
|
||||||
const filePath = path.join(promptsDir, filename);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return fs.readFileSync(filePath, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
// In production, if the file is missing, provide a helpful error
|
|
||||||
throw new Error(
|
|
||||||
`Failed to load prompt file: ${filename}. ` +
|
|
||||||
`Expected at: ${filePath}. ` +
|
|
||||||
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group Chat Prompts
|
|
||||||
export const groupChatModeratorSystemPrompt = loadPrompt('group-chat-moderator-system.md');
|
|
||||||
export const groupChatModeratorSynthesisPrompt = loadPrompt('group-chat-moderator-synthesis.md');
|
|
||||||
export const groupChatParticipantPrompt = loadPrompt('group-chat-participant.md');
|
|
||||||
export const groupChatParticipantRequestPrompt = loadPrompt('group-chat-participant-request.md');
|
|
||||||
@@ -1,34 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Centralized prompts module
|
* Centralized prompts module
|
||||||
*
|
*
|
||||||
* All built-in prompts are stored as .md files in this directory
|
* All prompts are stored as .md files in this directory and compiled
|
||||||
* and imported at build time using Vite's ?raw suffix.
|
* to TypeScript at build time by scripts/generate-prompts.mjs.
|
||||||
|
*
|
||||||
|
* The generated file is at src/generated/prompts.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Wizard prompts
|
|
||||||
import wizardSystemPrompt from './wizard-system.md?raw';
|
|
||||||
import wizardSystemContinuationPrompt from './wizard-system-continuation.md?raw';
|
|
||||||
import wizardDocumentGenerationPrompt from './wizard-document-generation.md?raw';
|
|
||||||
|
|
||||||
// AutoRun prompts
|
|
||||||
import autorunDefaultPrompt from './autorun-default.md?raw';
|
|
||||||
import autorunSynopsisPrompt from './autorun-synopsis.md?raw';
|
|
||||||
|
|
||||||
// Input processing prompts
|
|
||||||
import imageOnlyDefaultPrompt from './image-only-default.md?raw';
|
|
||||||
|
|
||||||
// Built-in command prompts
|
|
||||||
import commitCommandPrompt from './commit-command.md?raw';
|
|
||||||
|
|
||||||
// Maestro system prompt (injected at agent startup)
|
|
||||||
import maestroSystemPrompt from './maestro-system-prompt.md?raw';
|
|
||||||
|
|
||||||
// Group chat prompts (used by main process via src/main/prompts.ts)
|
|
||||||
import groupChatModeratorSystemPrompt from './group-chat-moderator-system.md?raw';
|
|
||||||
import groupChatModeratorSynthesisPrompt from './group-chat-moderator-synthesis.md?raw';
|
|
||||||
import groupChatParticipantPrompt from './group-chat-participant.md?raw';
|
|
||||||
import groupChatParticipantRequestPrompt from './group-chat-participant-request.md?raw';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// Wizard
|
// Wizard
|
||||||
wizardSystemPrompt,
|
wizardSystemPrompt,
|
||||||
@@ -53,4 +31,4 @@ export {
|
|||||||
groupChatModeratorSynthesisPrompt,
|
groupChatModeratorSynthesisPrompt,
|
||||||
groupChatParticipantPrompt,
|
groupChatParticipantPrompt,
|
||||||
groupChatParticipantRequestPrompt,
|
groupChatParticipantRequestPrompt,
|
||||||
};
|
} from '../generated/prompts';
|
||||||
|
|||||||
@@ -7785,11 +7785,6 @@ export default function MaestroConsole() {
|
|||||||
onTabChange={handleGroupChatRightTabChange}
|
onTabChange={handleGroupChatRightTabChange}
|
||||||
onJumpToMessage={handleJumpToGroupChatMessage}
|
onJumpToMessage={handleJumpToGroupChatMessage}
|
||||||
onColorsComputed={setGroupChatParticipantColors}
|
onColorsComputed={setGroupChatParticipantColors}
|
||||||
onJumpToSession={(sessionId) => {
|
|
||||||
// Dismiss group chat and switch to the participant's session
|
|
||||||
setActiveGroupChatId(null);
|
|
||||||
setActiveSessionId(sessionId);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -8392,6 +8387,42 @@ export default function MaestroConsole() {
|
|||||||
onResumeSession={handleResumeSession}
|
onResumeSession={handleResumeSession}
|
||||||
onOpenSessionAsTab={handleResumeSession}
|
onOpenSessionAsTab={handleResumeSession}
|
||||||
onOpenAboutModal={() => setAboutModalOpen(true)}
|
onOpenAboutModal={() => setAboutModalOpen(true)}
|
||||||
|
onFileClick={async (relativePath: string) => {
|
||||||
|
if (!activeSession) return;
|
||||||
|
const filename = relativePath.split('/').pop() || relativePath;
|
||||||
|
|
||||||
|
// Check if file should be opened externally (PDF, etc.)
|
||||||
|
if (shouldOpenExternally(filename)) {
|
||||||
|
const fullPath = `${activeSession.fullPath}/${relativePath}`;
|
||||||
|
window.maestro.shell.openExternal(`file://${fullPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullPath = `${activeSession.fullPath}/${relativePath}`;
|
||||||
|
const content = await window.maestro.fs.readFile(fullPath);
|
||||||
|
const newFile = {
|
||||||
|
name: filename,
|
||||||
|
content,
|
||||||
|
path: fullPath
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add to history if it's a different file than the current one
|
||||||
|
const currentFile = filePreviewHistory[filePreviewHistoryIndex];
|
||||||
|
if (!currentFile || currentFile.path !== fullPath) {
|
||||||
|
// Add to navigation history (truncate forward history if we're not at the end)
|
||||||
|
const newHistory = filePreviewHistory.slice(0, filePreviewHistoryIndex + 1);
|
||||||
|
newHistory.push(newFile);
|
||||||
|
setFilePreviewHistory(newHistory);
|
||||||
|
setFilePreviewHistoryIndex(newHistory.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewFile(newFile);
|
||||||
|
setActiveFocus('main');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[onFileClick] Failed to read file:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -667,6 +667,12 @@ export function DocumentsPanel({
|
|||||||
const items = [...prev];
|
const items = [...prev];
|
||||||
if (currentIsCopyDrag) {
|
if (currentIsCopyDrag) {
|
||||||
const original = items[draggedIndex];
|
const original = items[draggedIndex];
|
||||||
|
// Enable reset on ALL documents with the same filename (since duplicates require reset)
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].filename === original.filename) {
|
||||||
|
items[i] = { ...items[i], resetOnCompletion: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
items.splice(currentDropTargetIndex, 0, {
|
items.splice(currentDropTargetIndex, 0, {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
filename: original.filename,
|
filename: original.filename,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* This panel replaces the RightPanel when a group chat is active.
|
* This panel replaces the RightPanel when a group chat is active.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
import { PanelRightClose } from 'lucide-react';
|
import { PanelRightClose } from 'lucide-react';
|
||||||
import type { Theme, GroupChatParticipant, SessionState, Shortcut } from '../types';
|
import type { Theme, GroupChatParticipant, SessionState, Shortcut } from '../types';
|
||||||
import { ParticipantCard } from './ParticipantCard';
|
import { ParticipantCard } from './ParticipantCard';
|
||||||
@@ -22,6 +22,8 @@ interface GroupChatParticipantsProps {
|
|||||||
width: number;
|
width: number;
|
||||||
setWidthState: (width: number) => void;
|
setWidthState: (width: number) => void;
|
||||||
shortcuts: Record<string, Shortcut>;
|
shortcuts: Record<string, Shortcut>;
|
||||||
|
/** Group chat ID */
|
||||||
|
groupChatId: string;
|
||||||
/** Moderator agent ID (e.g., 'claude-code') */
|
/** Moderator agent ID (e.g., 'claude-code') */
|
||||||
moderatorAgentId: string;
|
moderatorAgentId: string;
|
||||||
/** Moderator internal session ID (for routing) */
|
/** Moderator internal session ID (for routing) */
|
||||||
@@ -43,6 +45,7 @@ export function GroupChatParticipants({
|
|||||||
width,
|
width,
|
||||||
setWidthState,
|
setWidthState,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
|
groupChatId,
|
||||||
moderatorAgentId,
|
moderatorAgentId,
|
||||||
moderatorSessionId,
|
moderatorSessionId,
|
||||||
moderatorAgentSessionId,
|
moderatorAgentSessionId,
|
||||||
@@ -74,6 +77,15 @@ export function GroupChatParticipants({
|
|||||||
return [...participants].sort((a, b) => a.name.localeCompare(b.name));
|
return [...participants].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, [participants]);
|
}, [participants]);
|
||||||
|
|
||||||
|
// Handle context reset for a participant
|
||||||
|
const handleContextReset = useCallback(async (participantName: string) => {
|
||||||
|
try {
|
||||||
|
await window.maestro.groupChat.resetParticipantContext(groupChatId, participantName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to reset context for ${participantName}:`, error);
|
||||||
|
}
|
||||||
|
}, [groupChatId]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -131,7 +143,7 @@ export function GroupChatParticipants({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
{/* Moderator card always at top */}
|
{/* Moderator card always at top - no reset for moderator */}
|
||||||
<ParticipantCard
|
<ParticipantCard
|
||||||
key="moderator"
|
key="moderator"
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -166,6 +178,8 @@ export function GroupChatParticipants({
|
|||||||
participant={participant}
|
participant={participant}
|
||||||
state={participantStates.get(participant.sessionId) || 'idle'}
|
state={participantStates.get(participant.sessionId) || 'idle'}
|
||||||
color={participantColors[participant.name]}
|
color={participantColors[participant.name]}
|
||||||
|
groupChatId={groupChatId}
|
||||||
|
onContextReset={handleContextReset}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ interface GroupChatRightPanelProps {
|
|||||||
onJumpToMessage?: (timestamp: number) => void;
|
onJumpToMessage?: (timestamp: number) => void;
|
||||||
/** Callback when participant colors are computed (for sharing with other components) */
|
/** Callback when participant colors are computed (for sharing with other components) */
|
||||||
onColorsComputed?: (colors: Record<string, string>) => void;
|
onColorsComputed?: (colors: Record<string, string>) => void;
|
||||||
/** Callback when user clicks to jump to a participant's session */
|
|
||||||
onJumpToSession?: (sessionId: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupChatRightPanel({
|
export function GroupChatRightPanel({
|
||||||
@@ -77,7 +75,6 @@ export function GroupChatRightPanel({
|
|||||||
onTabChange,
|
onTabChange,
|
||||||
onJumpToMessage,
|
onJumpToMessage,
|
||||||
onColorsComputed,
|
onColorsComputed,
|
||||||
onJumpToSession,
|
|
||||||
}: GroupChatRightPanelProps): JSX.Element | null {
|
}: GroupChatRightPanelProps): JSX.Element | null {
|
||||||
// Color preferences state
|
// Color preferences state
|
||||||
const [colorPreferences, setColorPreferences] = useState<Record<string, number>>({});
|
const [colorPreferences, setColorPreferences] = useState<Record<string, number>>({});
|
||||||
@@ -161,6 +158,15 @@ export function GroupChatRightPanel({
|
|||||||
return [...participants].sort((a, b) => a.name.localeCompare(b.name));
|
return [...participants].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, [participants]);
|
}, [participants]);
|
||||||
|
|
||||||
|
// Handle context reset for a participant
|
||||||
|
const handleContextReset = useCallback(async (participantName: string) => {
|
||||||
|
try {
|
||||||
|
await window.maestro.groupChat.resetParticipantContext(groupChatId, participantName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to reset context for ${participantName}:`, error);
|
||||||
|
}
|
||||||
|
}, [groupChatId]);
|
||||||
|
|
||||||
// History entries state
|
// History entries state
|
||||||
const [historyEntries, setHistoryEntries] = useState<GroupChatHistoryEntry[]>([]);
|
const [historyEntries, setHistoryEntries] = useState<GroupChatHistoryEntry[]>([]);
|
||||||
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
|
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
|
||||||
@@ -285,7 +291,6 @@ export function GroupChatRightPanel({
|
|||||||
participant={moderatorParticipant}
|
participant={moderatorParticipant}
|
||||||
state={moderatorState}
|
state={moderatorState}
|
||||||
color={participantColors['Moderator']}
|
color={participantColors['Moderator']}
|
||||||
onJumpToSession={onJumpToSession}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Separator between moderator and participants */}
|
{/* Separator between moderator and participants */}
|
||||||
@@ -318,7 +323,8 @@ export function GroupChatRightPanel({
|
|||||||
participant={participant}
|
participant={participant}
|
||||||
state={sessionState}
|
state={sessionState}
|
||||||
color={participantColors[participant.name]}
|
color={participantColors[participant.name]}
|
||||||
onJumpToSession={onJumpToSession}
|
groupChatId={groupChatId}
|
||||||
|
onContextReset={handleContextReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { X, Bot, User, Copy, Check, CheckCircle, XCircle, Trash2, Clock, Cpu, Zap, Play, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { X, Bot, User, Copy, Check, CheckCircle, XCircle, Trash2, Clock, Cpu, Zap, Play, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import type { Theme, HistoryEntry } from '../types';
|
import type { Theme, HistoryEntry } from '../types';
|
||||||
|
import type { FileNode } from '../types/fileTree';
|
||||||
import { useLayerStack } from '../contexts/LayerStackContext';
|
import { useLayerStack } from '../contexts/LayerStackContext';
|
||||||
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
||||||
import { formatElapsedTime } from '../utils/formatters';
|
import { formatElapsedTime } from '../utils/formatters';
|
||||||
@@ -27,6 +28,11 @@ interface HistoryDetailModalProps {
|
|||||||
filteredEntries?: HistoryEntry[];
|
filteredEntries?: HistoryEntry[];
|
||||||
currentIndex?: number;
|
currentIndex?: number;
|
||||||
onNavigate?: (entry: HistoryEntry, index: number) => void;
|
onNavigate?: (entry: HistoryEntry, index: number) => void;
|
||||||
|
// File linking props for markdown rendering
|
||||||
|
fileTree?: FileNode[];
|
||||||
|
cwd?: string;
|
||||||
|
projectRoot?: string;
|
||||||
|
onFileClick?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get context bar color based on usage percentage
|
// Get context bar color based on usage percentage
|
||||||
@@ -46,7 +52,11 @@ export function HistoryDetailModal({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
filteredEntries,
|
filteredEntries,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
onNavigate
|
onNavigate,
|
||||||
|
fileTree,
|
||||||
|
cwd,
|
||||||
|
projectRoot,
|
||||||
|
onFileClick
|
||||||
}: HistoryDetailModalProps) {
|
}: HistoryDetailModalProps) {
|
||||||
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
|
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
|
||||||
const layerIdRef = useRef<string>();
|
const layerIdRef = useRef<string>();
|
||||||
@@ -403,6 +413,10 @@ export function HistoryDetailModal({
|
|||||||
content={cleanResponse}
|
content={cleanResponse}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCopy={(text) => navigator.clipboard.writeText(text)}
|
onCopy={(text) => navigator.clipboard.writeText(text)}
|
||||||
|
fileTree={fileTree}
|
||||||
|
cwd={cwd}
|
||||||
|
projectRoot={projectRoot}
|
||||||
|
onFileClick={onFileClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -399,6 +399,9 @@ interface HistoryPanelProps {
|
|||||||
onResumeSession?: (agentSessionId: string) => void;
|
onResumeSession?: (agentSessionId: string) => void;
|
||||||
onOpenSessionAsTab?: (agentSessionId: string) => void;
|
onOpenSessionAsTab?: (agentSessionId: string) => void;
|
||||||
onOpenAboutModal?: () => void; // For opening About/achievements panel from history entries
|
onOpenAboutModal?: () => void; // For opening About/achievements panel from history entries
|
||||||
|
// File linking props for history detail modal
|
||||||
|
fileTree?: any[];
|
||||||
|
onFileClick?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryPanelHandle {
|
export interface HistoryPanelHandle {
|
||||||
@@ -414,7 +417,7 @@ const LOAD_MORE_COUNT = 50; // Entries to add when scrolling
|
|||||||
// Module-level storage for scroll positions (persists across session switches)
|
// Module-level storage for scroll positions (persists across session switches)
|
||||||
const scrollPositionCache = new Map<string, number>();
|
const scrollPositionCache = new Map<string, number>();
|
||||||
|
|
||||||
export const HistoryPanel = React.memo(forwardRef<HistoryPanelHandle, HistoryPanelProps>(function HistoryPanel({ session, theme, onJumpToAgentSession, onResumeSession, onOpenSessionAsTab, onOpenAboutModal }, ref) {
|
export const HistoryPanel = React.memo(forwardRef<HistoryPanelHandle, HistoryPanelProps>(function HistoryPanel({ session, theme, onJumpToAgentSession, onResumeSession, onOpenSessionAsTab, onOpenAboutModal, fileTree, onFileClick }, ref) {
|
||||||
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
|
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
|
||||||
const [activeFilters, setActiveFilters] = useState<Set<HistoryEntryType>>(new Set(['AUTO', 'USER']));
|
const [activeFilters, setActiveFilters] = useState<Set<HistoryEntryType>>(new Set(['AUTO', 'USER']));
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -1120,6 +1123,11 @@ export const HistoryPanel = React.memo(forwardRef<HistoryPanelHandle, HistoryPan
|
|||||||
setDisplayCount(Math.min(index + LOAD_MORE_COUNT, allFilteredEntries.length));
|
setDisplayCount(Math.min(index + LOAD_MORE_COUNT, allFilteredEntries.length));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
// File linking props for markdown rendering
|
||||||
|
fileTree={fileTree}
|
||||||
|
cwd={session.cwd}
|
||||||
|
projectRoot={session.projectRoot}
|
||||||
|
onFileClick={onFileClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { memo, useMemo, useState, useEffect } from 'react';
|
import React, { memo, useMemo, useState, useEffect } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
import { Clipboard, Loader2, ImageOff } from 'lucide-react';
|
import { Clipboard, Loader2, ImageOff } from 'lucide-react';
|
||||||
@@ -220,6 +221,7 @@ export const MarkdownRenderer = memo(({ content, theme, onCopy, className = '',
|
|||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
|
rehypePlugins={[rehypeRaw]}
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, href, children, ...props }) => {
|
a: ({ node, href, children, ...props }) => {
|
||||||
// Check for maestro-file:// protocol OR data-maestro-file attribute
|
// Check for maestro-file:// protocol OR data-maestro-file attribute
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* session ID, context usage, stats, and cost.
|
* session ID, context usage, stats, and cost.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageSquare, ExternalLink, DollarSign } from 'lucide-react';
|
import { MessageSquare, Copy, Check, DollarSign, RotateCcw } from 'lucide-react';
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
import type { Theme, GroupChatParticipant, SessionState } from '../types';
|
import type { Theme, GroupChatParticipant, SessionState } from '../types';
|
||||||
import { getStatusColor } from '../utils/theme';
|
import { getStatusColor } from '../utils/theme';
|
||||||
import { formatCost } from '../utils/formatters';
|
import { formatCost } from '../utils/formatters';
|
||||||
@@ -15,8 +16,8 @@ interface ParticipantCardProps {
|
|||||||
participant: GroupChatParticipant;
|
participant: GroupChatParticipant;
|
||||||
state: SessionState;
|
state: SessionState;
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Callback when user clicks session ID pill to jump to the agent */
|
groupChatId?: string;
|
||||||
onJumpToSession?: (sessionId: string) => void;
|
onContextReset?: (participantName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,17 +36,23 @@ export function ParticipantCard({
|
|||||||
participant,
|
participant,
|
||||||
state,
|
state,
|
||||||
color,
|
color,
|
||||||
onJumpToSession,
|
groupChatId,
|
||||||
|
onContextReset,
|
||||||
}: ParticipantCardProps): JSX.Element {
|
}: ParticipantCardProps): JSX.Element {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
|
||||||
// Use agent's session ID (clean GUID) when available, otherwise show pending
|
// Use agent's session ID (clean GUID) when available, otherwise show pending
|
||||||
const agentSessionId = participant.agentSessionId;
|
const agentSessionId = participant.agentSessionId;
|
||||||
const isPending = !agentSessionId;
|
const isPending = !agentSessionId;
|
||||||
|
|
||||||
const handleJumpToSession = () => {
|
const copySessionId = useCallback(() => {
|
||||||
if (onJumpToSession && participant.sessionId) {
|
if (agentSessionId) {
|
||||||
onJumpToSession(participant.sessionId);
|
navigator.clipboard.writeText(agentSessionId);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
}
|
}
|
||||||
};
|
}, [agentSessionId]);
|
||||||
|
|
||||||
// Determine if state should animate (busy or connecting)
|
// Determine if state should animate (busy or connecting)
|
||||||
const shouldPulse = state === 'busy' || state === 'connecting';
|
const shouldPulse = state === 'busy' || state === 'connecting';
|
||||||
@@ -66,6 +73,19 @@ export function ParticipantCard({
|
|||||||
// Context usage percentage (default to 0 if not set)
|
// Context usage percentage (default to 0 if not set)
|
||||||
const contextUsage = participant.contextUsage ?? 0;
|
const contextUsage = participant.contextUsage ?? 0;
|
||||||
|
|
||||||
|
// Show reset button when context usage is 40% or higher
|
||||||
|
const showResetButton = contextUsage >= 40 && onContextReset && groupChatId && !isResetting;
|
||||||
|
|
||||||
|
const handleReset = useCallback(async () => {
|
||||||
|
if (!onContextReset || !groupChatId) return;
|
||||||
|
setIsResetting(true);
|
||||||
|
try {
|
||||||
|
await onContextReset(participant.name);
|
||||||
|
} finally {
|
||||||
|
setIsResetting(false);
|
||||||
|
}
|
||||||
|
}, [onContextReset, groupChatId, participant.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-lg border p-3"
|
className="rounded-lg border p-3"
|
||||||
@@ -105,19 +125,23 @@ export function ParticipantCard({
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleJumpToSession}
|
onClick={copySessionId}
|
||||||
className="flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full hover:opacity-80 transition-opacity cursor-pointer shrink-0"
|
className="flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full hover:opacity-80 transition-opacity cursor-pointer shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${theme.colors.accent}20`,
|
backgroundColor: `${theme.colors.accent}20`,
|
||||||
color: theme.colors.accent,
|
color: theme.colors.accent,
|
||||||
border: `1px solid ${theme.colors.accent}40`,
|
border: `1px solid ${theme.colors.accent}40`,
|
||||||
}}
|
}}
|
||||||
title={`Session: ${agentSessionId}\nClick to jump to agent`}
|
title={`Session: ${agentSessionId}\nClick to copy`}
|
||||||
>
|
>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{agentSessionId.slice(0, 8).toUpperCase()}
|
{agentSessionId.slice(0, 8).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<ExternalLink className="w-2.5 h-2.5" />
|
{copied ? (
|
||||||
|
<Check className="w-2.5 h-2.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-2.5 h-2.5" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +214,35 @@ export function ParticipantCard({
|
|||||||
{formatCost(participant.totalCost).slice(1)}
|
{formatCost(participant.totalCost).slice(1)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* Reset button - shown when context usage >= 40% */}
|
||||||
|
{showResetButton && (
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
className="flex items-center gap-0.5 text-[10px] px-1.5 py-0.5 rounded shrink-0 hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${theme.colors.warning}20`,
|
||||||
|
color: theme.colors.warning,
|
||||||
|
border: `1px solid ${theme.colors.warning}40`,
|
||||||
|
}}
|
||||||
|
title="Reset context: Summarize current session and start fresh"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3" />
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* Reset in progress indicator */}
|
||||||
|
{isResetting && (
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-0.5 text-[10px] px-1.5 py-0.5 rounded shrink-0 animate-pulse"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${theme.colors.warning}20`,
|
||||||
|
color: theme.colors.warning,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3 animate-spin" />
|
||||||
|
Resetting...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ interface RightPanelProps {
|
|||||||
onResumeSession?: (agentSessionId: string) => void;
|
onResumeSession?: (agentSessionId: string) => void;
|
||||||
onOpenSessionAsTab?: (agentSessionId: string) => void;
|
onOpenSessionAsTab?: (agentSessionId: string) => void;
|
||||||
onOpenAboutModal?: () => void; // For opening About/achievements panel from history entries
|
onOpenAboutModal?: () => void; // For opening About/achievements panel from history entries
|
||||||
|
// File linking callback for history detail modal
|
||||||
|
onFileClick?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function RightPanel(props, ref) {
|
export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function RightPanel(props, ref) {
|
||||||
@@ -112,7 +114,7 @@ export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function
|
|||||||
// Error handling callbacks (Phase 5.10)
|
// Error handling callbacks (Phase 5.10)
|
||||||
onSkipCurrentDocument, onAbortBatchOnError, onResumeAfterError,
|
onSkipCurrentDocument, onAbortBatchOnError, onResumeAfterError,
|
||||||
onJumpToAgentSession, onResumeSession,
|
onJumpToAgentSession, onResumeSession,
|
||||||
onOpenSessionAsTab, onOpenAboutModal
|
onOpenSessionAsTab, onOpenAboutModal, onFileClick
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const historyPanelRef = useRef<HistoryPanelHandle>(null);
|
const historyPanelRef = useRef<HistoryPanelHandle>(null);
|
||||||
@@ -461,6 +463,8 @@ export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function
|
|||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
onOpenSessionAsTab={onOpenSessionAsTab}
|
onOpenSessionAsTab={onOpenSessionAsTab}
|
||||||
onOpenAboutModal={onOpenAboutModal}
|
onOpenAboutModal={onOpenAboutModal}
|
||||||
|
fileTree={filteredFileTree}
|
||||||
|
onFileClick={onFileClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -990,6 +990,7 @@ interface MaestroAPI {
|
|||||||
}>;
|
}>;
|
||||||
sendToParticipant: (id: string, name: string, message: string, images?: string[]) => Promise<void>;
|
sendToParticipant: (id: string, name: string, message: string, images?: string[]) => Promise<void>;
|
||||||
removeParticipant: (id: string, name: string) => Promise<void>;
|
removeParticipant: (id: string, name: string) => Promise<void>;
|
||||||
|
resetParticipantContext: (id: string, name: string, cwd?: string) => Promise<{ newAgentSessionId: string }>;
|
||||||
// History
|
// History
|
||||||
getHistory: (id: string) => Promise<Array<{
|
getHistory: (id: string) => Promise<Array<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user