## CHANGES

- Share release-to-release project evolution once you provide repository data. 🚀
This commit is contained in:
Pedram Amini
2025-12-21 16:22:20 -06:00
parent a20e7b6200
commit 39825d8d25
8 changed files with 177 additions and 23 deletions

View File

@@ -0,0 +1,127 @@
// Clean playbooks command
// Removes orphaned playbooks (playbooks for sessions that no longer exist)
import * as fs from 'fs';
import * as path from 'path';
import { readSessions, getConfigDirectory } from '../services/storage';
import { formatError, formatSuccess } from '../output/formatter';
interface CleanPlaybooksOptions {
json?: boolean;
dryRun?: boolean;
}
/**
* Get the playbooks directory path
*/
function getPlaybooksDir(): string {
return path.join(getConfigDirectory(), 'playbooks');
}
/**
* Find orphaned playbook files (files for sessions that no longer exist)
*/
function findOrphanedPlaybooks(): Array<{ sessionId: string; filePath: string }> {
const playbooksDir = getPlaybooksDir();
const orphaned: Array<{ sessionId: string; filePath: string }> = [];
try {
if (!fs.existsSync(playbooksDir)) {
return orphaned;
}
const sessions = readSessions();
const sessionIds = new Set(sessions.map((s) => s.id));
const files = fs.readdirSync(playbooksDir);
for (const file of files) {
if (!file.endsWith('.json')) continue;
const sessionId = file.replace('.json', '');
if (!sessionIds.has(sessionId)) {
orphaned.push({
sessionId,
filePath: path.join(playbooksDir, file),
});
}
}
return orphaned;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return orphaned;
}
throw error;
}
}
export function cleanPlaybooks(options: CleanPlaybooksOptions): void {
try {
const orphaned = findOrphanedPlaybooks();
if (orphaned.length === 0) {
if (options.json) {
console.log(JSON.stringify({ removed: [], count: 0 }));
} else {
console.log(formatSuccess('No orphaned playbooks found'));
}
return;
}
if (options.dryRun) {
if (options.json) {
console.log(
JSON.stringify({
dryRun: true,
wouldRemove: orphaned.map((o) => ({
sessionId: o.sessionId,
filePath: o.filePath,
})),
count: orphaned.length,
})
);
} else {
console.log(`\nWould remove ${orphaned.length} orphaned playbook file(s):\n`);
for (const o of orphaned) {
console.log(` ${o.sessionId.slice(0, 8)} ${o.filePath}`);
}
console.log('\nRun without --dry-run to actually remove these files.');
}
return;
}
// Actually remove the files
const removed: string[] = [];
for (const o of orphaned) {
try {
fs.unlinkSync(o.filePath);
removed.push(o.sessionId);
} catch (error) {
console.error(`Failed to remove ${o.filePath}: ${error}`);
}
}
if (options.json) {
console.log(
JSON.stringify({
removed: removed.map((id) => id.slice(0, 8)),
count: removed.length,
})
);
} else {
console.log(formatSuccess(`Removed ${removed.length} orphaned playbook file(s)`));
for (const id of removed) {
console.log(` ${id.slice(0, 8)}`);
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
if (options.json) {
console.error(JSON.stringify({ error: message }));
} else {
console.error(formatError(`Failed to clean playbooks: ${message}`));
}
process.exit(1);
}
}

View File

@@ -11,6 +11,7 @@ import { listPlaybooks } from './commands/list-playbooks';
import { showPlaybook } from './commands/show-playbook';
import { showAgent } from './commands/show-agent';
import { runPlaybook } from './commands/run-playbook';
import { cleanPlaybooks } from './commands/clean-playbooks';
// Read version from package.json at runtime
function getVersion(): string {
@@ -81,4 +82,14 @@ program
.option('--wait', 'Wait for agent to become available if busy')
.action(runPlaybook);
// Clean command
const clean = program.command('clean').description('Clean up orphaned resources');
clean
.command('playbooks')
.description('Remove playbooks for deleted sessions')
.option('--dry-run', 'Show what would be removed without actually removing')
.option('--json', 'Output as JSON (for scripting)')
.action(cleanPlaybooks);
program.parse();

View File

@@ -216,6 +216,24 @@ export function registerPlaybooksHandlers(deps: PlaybooksHandlerDependencies): v
})
);
// Delete all playbooks for a session (used when session is deleted)
ipcMain.handle(
'playbooks:deleteAll',
createIpcHandler(handlerOpts('deleteAll'), async (sessionId: string) => {
const filePath = getPlaybooksFilePath(app, sessionId);
try {
await fs.unlink(filePath);
logger.info(`Deleted all playbooks for session ${sessionId}`, LOG_CONTEXT);
} catch (error) {
// File doesn't exist, that's fine
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
return {};
})
);
// Export a playbook as a ZIP file
ipcMain.handle(
'playbooks:export',

View File

@@ -907,6 +907,8 @@ contextBridge.exposeInMainWorld('maestro', {
) => ipcRenderer.invoke('playbooks:update', sessionId, playbookId, updates),
delete: (sessionId: string, playbookId: string) =>
ipcRenderer.invoke('playbooks:delete', sessionId, playbookId),
deleteAll: (sessionId: string) =>
ipcRenderer.invoke('playbooks:deleteAll', sessionId),
export: (sessionId: string, playbookId: string, autoRunFolderPath: string) =>
ipcRenderer.invoke('playbooks:export', sessionId, playbookId, autoRunFolderPath),
import: (sessionId: string, autoRunFolderPath: string) =>
@@ -1796,6 +1798,10 @@ export interface MaestroAPI {
success: boolean;
error?: string;
}>;
deleteAll: (sessionId: string) => Promise<{
success: boolean;
error?: string;
}>;
};
debug: {
createPackage: (options?: {

View File

@@ -3541,6 +3541,13 @@ export default function MaestroConsole() {
console.error('Failed to kill terminal process:', error);
}
// Delete associated playbooks
try {
await window.maestro.playbooks.deleteAll(id);
} catch (error) {
console.error('Failed to delete playbooks:', error);
}
const newSessions = sessions.filter(s => s.id !== id);
setSessions(newSessions);
// Flush immediately for critical operation (session deletion)

View File

@@ -750,7 +750,7 @@ export function AgentSessionsBrowser({
</span>
</div>
{/* Context Window with visual gauge */}
{/* Total Tokens */}
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-1">
<Zap className="w-4 h-4" style={{ color: theme.colors.accent }} />
@@ -761,32 +761,13 @@ export function AgentSessionsBrowser({
{(() => {
const totalTokens = viewingSession.inputTokens + viewingSession.outputTokens;
const contextUsage = Math.min(100, (totalTokens / 200000) * 100);
const getContextColor = (usage: number) => {
if (usage >= 90) return theme.colors.error;
if (usage >= 70) return theme.colors.warning;
return theme.colors.accent;
};
return (
<>
<span className="text-lg font-mono font-semibold" style={{ color: theme.colors.textMain }}>
{formatNumber(totalTokens)}
</span>
<div className="flex items-center gap-2 mt-1">
<div className="w-24 h-2 rounded-full overflow-hidden" style={{ backgroundColor: theme.colors.border }}>
<div
className="h-full transition-all duration-500 ease-out"
style={{
width: `${contextUsage}%`,
backgroundColor: getContextColor(contextUsage)
}}
/>
</div>
<span className="text-[10px] font-mono font-bold" style={{ color: getContextColor(contextUsage) }}>
{contextUsage.toFixed(1)}%
</span>
</div>
<span className="text-[10px] mt-0.5" style={{ color: theme.colors.textDim }}>
of 200k context
of 200k context <span className="font-mono font-medium" style={{ color: theme.colors.accent }}>{contextUsage.toFixed(1)}%</span>
</span>
</>
);

View File

@@ -190,10 +190,13 @@ export function ProcessMonitor(props: ProcessMonitorProps) {
containerRef.current?.focus();
}, []);
// Focus detail view when it opens
// Focus detail view when it opens, restore focus to container when it closes
useEffect(() => {
if (detailView && detailViewRef.current) {
detailViewRef.current.focus();
} else if (!detailView && containerRef.current) {
// Restore focus to the container when returning from detail view
containerRef.current.focus();
}
}, [detailView]);
@@ -1035,7 +1038,7 @@ export function ProcessMonitor(props: ProcessMonitorProps) {
</div>
{/* Detail Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin p-6 space-y-6">
{/* Process Name & Status */}
<div className="flex items-center gap-3">
<div

View File

@@ -743,6 +743,7 @@ interface MaestroAPI {
};
}>) => Promise<{ success: boolean; playbook?: any; error?: string }>;
delete: (sessionId: string, playbookId: string) => Promise<{ success: boolean; error?: string }>;
deleteAll: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
export: (sessionId: string, playbookId: string, autoRunFolderPath: string) => Promise<{ success: boolean; filePath?: string; error?: string }>;
import: (sessionId: string, autoRunFolderPath: string) => Promise<{ success: boolean; playbook?: any; importedDocs?: string[]; error?: string }>;
};