mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
refactor: Cross-platform build scripts and session format cleanup
- Add scripts/set-version.mjs for cross-platform VITE_APP_VERSION setting
(replaces bash-specific env var syntax that failed on Windows)
- Extract magic numbers into CLAUDE_SESSION_PARSE_LIMITS and CLAUDE_PRICING
constants for better maintainability
- Remove legacy session format migration code - sessions now require aiTabs
- Fix session ID regex patterns to properly parse -ai-{tabId} format
- Remove deprecated aiLogs fallbacks - logs are exclusively in aiTabs now
Claude ID: bfd92ffb-a375-47be-94c5-fe4186325092
Maestro ID: b9bc0d08-5be2-4fdf-93cd-5618a8d53b35
This commit is contained in:
@@ -22,10 +22,10 @@
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
"build:renderer": "vite build",
|
||||
"build:web": "vite build --config vite.config.web.mts",
|
||||
"package": "VITE_APP_VERSION=\"LOCAL $(git rev-parse --short=8 HEAD)\" npm run build && electron-builder --mac --win --linux",
|
||||
"package:mac": "VITE_APP_VERSION=\"LOCAL $(git rev-parse --short=8 HEAD)\" npm run build && electron-builder --mac",
|
||||
"package:win": "VITE_APP_VERSION=\"LOCAL $(git rev-parse --short=8 HEAD)\" npm run build && electron-builder --win",
|
||||
"package:linux": "VITE_APP_VERSION=\"LOCAL $(git rev-parse --short=8 HEAD)\" npm run build && electron-builder --linux",
|
||||
"package": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --mac --win --linux",
|
||||
"package:mac": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --mac",
|
||||
"package:win": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --win",
|
||||
"package:linux": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --linux",
|
||||
"start": "electron .",
|
||||
"clean": "rm -rf dist release node_modules/.vite",
|
||||
"postinstall": "electron-rebuild -f -w node-pty"
|
||||
|
||||
56
scripts/set-version.mjs
Normal file
56
scripts/set-version.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Cross-platform script to set VITE_APP_VERSION environment variable
|
||||
* with the local git hash. Works on Windows, macOS, and Linux.
|
||||
*
|
||||
* Usage: node scripts/set-version.mjs <command> [args...]
|
||||
* Example: node scripts/set-version.mjs npm run build
|
||||
*/
|
||||
|
||||
import { execFileSync, spawn } from 'child_process';
|
||||
import process from 'process';
|
||||
|
||||
function getGitHash() {
|
||||
try {
|
||||
const hash = execFileSync('git', ['rev-parse', '--short=8', 'HEAD'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim();
|
||||
return hash;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const gitHash = getGitHash();
|
||||
const version = `LOCAL ${gitHash}`;
|
||||
|
||||
// Set environment variable
|
||||
process.env.VITE_APP_VERSION = version;
|
||||
|
||||
// Get the command and args to run
|
||||
const [,, ...args] = process.argv;
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log(`VITE_APP_VERSION=${version}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run the command with the environment variable set
|
||||
const command = args[0];
|
||||
const commandArgs = args.slice(1);
|
||||
|
||||
const child = spawn(command, commandArgs, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, VITE_APP_VERSION: version }
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Failed to run command: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -10,6 +10,28 @@ import { detectShells } from './utils/shellDetector';
|
||||
import { getThemeById } from './themes';
|
||||
import Store from 'electron-store';
|
||||
|
||||
// Constants for Claude session parsing
|
||||
const CLAUDE_SESSION_PARSE_LIMITS = {
|
||||
/** Max lines to scan from start of file to find first user message */
|
||||
FIRST_MESSAGE_SCAN_LINES: 20,
|
||||
/** Max lines to scan from end of file to find last timestamp */
|
||||
LAST_TIMESTAMP_SCAN_LINES: 10,
|
||||
/** Max lines to scan for oldest timestamp in stats calculation */
|
||||
OLDEST_TIMESTAMP_SCAN_LINES: 5,
|
||||
/** Batch size for processing session files (allows UI updates) */
|
||||
STATS_BATCH_SIZE: 20,
|
||||
/** Max characters for first message preview */
|
||||
FIRST_MESSAGE_PREVIEW_LENGTH: 200,
|
||||
};
|
||||
|
||||
// Claude API pricing (per million tokens)
|
||||
const CLAUDE_PRICING = {
|
||||
INPUT_PER_MILLION: 3,
|
||||
OUTPUT_PER_MILLION: 15,
|
||||
CACHE_READ_PER_MILLION: 0.30,
|
||||
CACHE_CREATION_PER_MILLION: 3.75,
|
||||
};
|
||||
|
||||
// Type definitions
|
||||
interface MaestroSettings {
|
||||
activeThemeId: string;
|
||||
@@ -1503,7 +1525,7 @@ function setupIpcHandlers() {
|
||||
const messageCount = userMessageCount + assistantMessageCount;
|
||||
|
||||
// Extract first user message content - parse only first few lines
|
||||
for (let i = 0; i < Math.min(lines.length, 20); i++) {
|
||||
for (let i = 0; i < Math.min(lines.length, CLAUDE_SESSION_PARSE_LIMITS.FIRST_MESSAGE_SCAN_LINES); i++) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
if (entry.type === 'user' && entry.message?.content) {
|
||||
@@ -1540,18 +1562,16 @@ function setupIpcHandlers() {
|
||||
const cacheCreationMatches = content.matchAll(/"cache_creation_input_tokens"\s*:\s*(\d+)/g);
|
||||
for (const m of cacheCreationMatches) totalCacheCreationTokens += parseInt(m[1], 10);
|
||||
|
||||
// Calculate cost estimate using Claude Sonnet 4 pricing:
|
||||
// Input: $3 per million tokens, Output: $15 per million tokens
|
||||
// Cache read: $0.30 per million, Cache creation: $3.75 per million
|
||||
const inputCost = (totalInputTokens / 1_000_000) * 3;
|
||||
const outputCost = (totalOutputTokens / 1_000_000) * 15;
|
||||
const cacheReadCost = (totalCacheReadTokens / 1_000_000) * 0.30;
|
||||
const cacheCreationCost = (totalCacheCreationTokens / 1_000_000) * 3.75;
|
||||
// Calculate cost estimate using Claude Sonnet 4 pricing
|
||||
const inputCost = (totalInputTokens / 1_000_000) * CLAUDE_PRICING.INPUT_PER_MILLION;
|
||||
const outputCost = (totalOutputTokens / 1_000_000) * CLAUDE_PRICING.OUTPUT_PER_MILLION;
|
||||
const cacheReadCost = (totalCacheReadTokens / 1_000_000) * CLAUDE_PRICING.CACHE_READ_PER_MILLION;
|
||||
const cacheCreationCost = (totalCacheCreationTokens / 1_000_000) * CLAUDE_PRICING.CACHE_CREATION_PER_MILLION;
|
||||
const costUsd = inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
||||
|
||||
// Extract last timestamp from the session to calculate duration
|
||||
let lastTimestamp = timestamp;
|
||||
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 10); i--) {
|
||||
for (let i = lines.length - 1; i >= Math.max(0, lines.length - CLAUDE_SESSION_PARSE_LIMITS.LAST_TIMESTAMP_SCAN_LINES); i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
if (entry.timestamp) {
|
||||
@@ -1573,7 +1593,7 @@ function setupIpcHandlers() {
|
||||
projectPath,
|
||||
timestamp,
|
||||
modifiedAt: stats.mtime.toISOString(),
|
||||
firstMessage: firstUserMessage.slice(0, 200), // Truncate for display
|
||||
firstMessage: firstUserMessage.slice(0, CLAUDE_SESSION_PARSE_LIMITS.FIRST_MESSAGE_PREVIEW_LENGTH), // Truncate for display
|
||||
messageCount,
|
||||
sizeBytes: stats.size,
|
||||
costUsd,
|
||||
@@ -1708,7 +1728,7 @@ function setupIpcHandlers() {
|
||||
const messageCount = userMessageCount + assistantMessageCount;
|
||||
|
||||
// Extract first user message content - parse only first few lines
|
||||
for (let i = 0; i < Math.min(lines.length, 20); i++) {
|
||||
for (let i = 0; i < Math.min(lines.length, CLAUDE_SESSION_PARSE_LIMITS.FIRST_MESSAGE_SCAN_LINES); i++) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
if (entry.type === 'user' && entry.message?.content) {
|
||||
@@ -1742,15 +1762,15 @@ function setupIpcHandlers() {
|
||||
for (const m of cacheCreationMatches) totalCacheCreationTokens += parseInt(m[1], 10);
|
||||
|
||||
// Calculate cost estimate
|
||||
const inputCost = (totalInputTokens / 1_000_000) * 3;
|
||||
const outputCost = (totalOutputTokens / 1_000_000) * 15;
|
||||
const cacheReadCost = (totalCacheReadTokens / 1_000_000) * 0.30;
|
||||
const cacheCreationCost = (totalCacheCreationTokens / 1_000_000) * 3.75;
|
||||
const inputCost = (totalInputTokens / 1_000_000) * CLAUDE_PRICING.INPUT_PER_MILLION;
|
||||
const outputCost = (totalOutputTokens / 1_000_000) * CLAUDE_PRICING.OUTPUT_PER_MILLION;
|
||||
const cacheReadCost = (totalCacheReadTokens / 1_000_000) * CLAUDE_PRICING.CACHE_READ_PER_MILLION;
|
||||
const cacheCreationCost = (totalCacheCreationTokens / 1_000_000) * CLAUDE_PRICING.CACHE_CREATION_PER_MILLION;
|
||||
const costUsd = inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
||||
|
||||
// Extract last timestamp for duration
|
||||
let lastTimestamp = timestamp;
|
||||
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 10); i--) {
|
||||
for (let i = lines.length - 1; i >= Math.max(0, lines.length - CLAUDE_SESSION_PARSE_LIMITS.LAST_TIMESTAMP_SCAN_LINES); i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
if (entry.timestamp) {
|
||||
@@ -1776,7 +1796,7 @@ function setupIpcHandlers() {
|
||||
projectPath,
|
||||
timestamp,
|
||||
modifiedAt: new Date(fileInfo.modifiedAt).toISOString(),
|
||||
firstMessage: firstUserMessage.slice(0, 200),
|
||||
firstMessage: firstUserMessage.slice(0, CLAUDE_SESSION_PARSE_LIMITS.FIRST_MESSAGE_PREVIEW_LENGTH),
|
||||
messageCount,
|
||||
sizeBytes: fileInfo.sizeBytes,
|
||||
costUsd,
|
||||
@@ -1858,10 +1878,10 @@ function setupIpcHandlers() {
|
||||
let processedCount = 0;
|
||||
|
||||
// Process files in batches to allow UI updates
|
||||
const BATCH_SIZE = 20;
|
||||
const batchSize = CLAUDE_SESSION_PARSE_LIMITS.STATS_BATCH_SIZE;
|
||||
|
||||
for (let i = 0; i < sessionFiles.length; i += BATCH_SIZE) {
|
||||
const batch = sessionFiles.slice(i, i + BATCH_SIZE);
|
||||
for (let i = 0; i < sessionFiles.length; i += batchSize) {
|
||||
const batch = sessionFiles.slice(i, i + batchSize);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async (filename) => {
|
||||
@@ -1896,15 +1916,15 @@ function setupIpcHandlers() {
|
||||
for (const m of cacheCreationMatches) cacheCreationTokens += parseInt(m[1], 10);
|
||||
|
||||
// Calculate cost
|
||||
const inputCost = (inputTokens / 1_000_000) * 3;
|
||||
const outputCost = (outputTokens / 1_000_000) * 15;
|
||||
const cacheReadCost = (cacheReadTokens / 1_000_000) * 0.30;
|
||||
const cacheCreationCost = (cacheCreationTokens / 1_000_000) * 3.75;
|
||||
const inputCost = (inputTokens / 1_000_000) * CLAUDE_PRICING.INPUT_PER_MILLION;
|
||||
const outputCost = (outputTokens / 1_000_000) * CLAUDE_PRICING.OUTPUT_PER_MILLION;
|
||||
const cacheReadCost = (cacheReadTokens / 1_000_000) * CLAUDE_PRICING.CACHE_READ_PER_MILLION;
|
||||
const cacheCreationCost = (cacheCreationTokens / 1_000_000) * CLAUDE_PRICING.CACHE_CREATION_PER_MILLION;
|
||||
totalCostUsd += inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
||||
|
||||
// Find oldest timestamp
|
||||
const lines = content.split('\n').filter(l => l.trim());
|
||||
for (let j = 0; j < Math.min(lines.length, 5); j++) {
|
||||
for (let j = 0; j < Math.min(lines.length, CLAUDE_SESSION_PARSE_LIMITS.OLDEST_TIMESTAMP_SCAN_LINES); j++) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[j]);
|
||||
if (entry.timestamp) {
|
||||
@@ -1923,7 +1943,7 @@ function setupIpcHandlers() {
|
||||
})
|
||||
);
|
||||
|
||||
processedCount = Math.min(i + BATCH_SIZE, sessionFiles.length);
|
||||
processedCount = Math.min(i + batchSize, sessionFiles.length);
|
||||
|
||||
// Send progressive update
|
||||
sendUpdate({
|
||||
@@ -3009,8 +3029,9 @@ function setupProcessListeners() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseSessionId = sessionId.replace(/-ai$|-batch-\d+$|-synopsis-\d+$/, '');
|
||||
const isAiOutput = sessionId.endsWith('-ai') || sessionId.includes('-batch-') || sessionId.includes('-synopsis-');
|
||||
// Extract base session ID from formats: {id}-ai-{tabId}, {id}-batch-{timestamp}, {id}-synopsis-{timestamp}
|
||||
const baseSessionId = sessionId.replace(/-ai-[^-]+$|-batch-\d+$|-synopsis-\d+$/, '');
|
||||
const isAiOutput = sessionId.includes('-ai-') || sessionId.includes('-batch-') || sessionId.includes('-synopsis-');
|
||||
const msgId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
console.log(`[WebBroadcast] Broadcasting session_output: msgId=${msgId}, session=${baseSessionId}, source=${isAiOutput ? 'ai' : 'terminal'}, dataLen=${data.length}`);
|
||||
webServer.broadcastToSessionClients(baseSessionId, {
|
||||
@@ -3029,7 +3050,8 @@ function setupProcessListeners() {
|
||||
|
||||
// Broadcast exit to web clients
|
||||
if (webServer) {
|
||||
const baseSessionId = sessionId.replace(/-ai$|-terminal$|-batch-\d+$|-synopsis-\d+$/, '');
|
||||
// Extract base session ID from formats: {id}-ai-{tabId}, {id}-terminal, {id}-batch-{timestamp}, {id}-synopsis-{timestamp}
|
||||
const baseSessionId = sessionId.replace(/-ai-[^-]+$|-terminal$|-batch-\d+$|-synopsis-\d+$/, '');
|
||||
webServer.broadcastToSessionClients(baseSessionId, {
|
||||
type: 'session_exit',
|
||||
sessionId: baseSessionId,
|
||||
|
||||
@@ -298,59 +298,18 @@ export default function MaestroConsole() {
|
||||
// Restore a persisted session by respawning its process
|
||||
const restoreSession = async (session: Session): Promise<Session> => {
|
||||
try {
|
||||
// ===== Migration: Convert old session format to new aiTabs format =====
|
||||
// If session lacks aiTabs array, migrate from legacy fields
|
||||
// Sessions must have aiTabs - if missing, this is a data corruption issue
|
||||
if (!session.aiTabs || session.aiTabs.length === 0) {
|
||||
// Look up starred status and session name from existing stores
|
||||
let isStarred = false;
|
||||
let sessionName: string | null = null;
|
||||
|
||||
if (session.claudeSessionId && session.cwd) {
|
||||
try {
|
||||
// Look up session metadata from Claude session origins (name and starred)
|
||||
const origins = await window.maestro.claude.getSessionOrigins(session.cwd);
|
||||
const originData = origins[session.claudeSessionId];
|
||||
if (originData && typeof originData === 'object') {
|
||||
if (originData.sessionName) {
|
||||
sessionName = originData.sessionName;
|
||||
}
|
||||
if (originData.starred !== undefined) {
|
||||
isStarred = originData.starred;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[restoreSession] Failed to lookup starred/named status during migration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial tab from legacy data
|
||||
const initialTab: AITab = {
|
||||
id: generateId(),
|
||||
claudeSessionId: session.claudeSessionId || null,
|
||||
name: sessionName,
|
||||
starred: isStarred,
|
||||
logs: session.aiLogs || [],
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
usageStats: session.usageStats,
|
||||
createdAt: Date.now(),
|
||||
state: 'idle'
|
||||
};
|
||||
|
||||
session = {
|
||||
console.error('[restoreSession] Session has no aiTabs - data corruption, skipping:', session.id);
|
||||
return {
|
||||
...session,
|
||||
aiTabs: [initialTab],
|
||||
activeTabId: initialTab.id,
|
||||
closedTabHistory: []
|
||||
aiPid: -1,
|
||||
terminalPid: -1,
|
||||
state: 'error' as SessionState,
|
||||
isLive: false,
|
||||
liveUrl: undefined
|
||||
};
|
||||
|
||||
console.log('[restoreSession] Migrated session to aiTabs format:', session.id, {
|
||||
claudeSessionId: initialTab.claudeSessionId,
|
||||
name: sessionName,
|
||||
starred: isStarred
|
||||
});
|
||||
}
|
||||
// ===== End Migration =====
|
||||
|
||||
// Detect and fix inputMode/toolType mismatch
|
||||
// The AI agent should never use 'terminal' as toolType
|
||||
@@ -362,16 +321,19 @@ export default function MaestroConsole() {
|
||||
console.warn(`[restoreSession] Session has toolType='terminal', using default agent for AI process`);
|
||||
aiAgentType = defaultAgent as ToolType;
|
||||
|
||||
const targetLogKey = 'aiLogs';
|
||||
correctedSession[targetLogKey] = [
|
||||
...correctedSession[targetLogKey],
|
||||
{
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: '⚠️ Using default AI agent (Claude Code) for this session.'
|
||||
}
|
||||
];
|
||||
// Add warning to the active tab's logs
|
||||
const warningLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: '⚠️ Using default AI agent (Claude Code) for this session.'
|
||||
};
|
||||
const activeTabIndex = correctedSession.aiTabs.findIndex(tab => tab.id === correctedSession.activeTabId);
|
||||
if (activeTabIndex >= 0) {
|
||||
correctedSession.aiTabs = correctedSession.aiTabs.map((tab, i) =>
|
||||
i === activeTabIndex ? { ...tab, logs: [...tab.logs, warningLog] } : tab
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get agent definitions for both processes
|
||||
@@ -407,10 +369,12 @@ export default function MaestroConsole() {
|
||||
let aiSpawnResult = { pid: 0, success: true }; // Default for batch mode
|
||||
|
||||
if (!isClaudeBatchMode) {
|
||||
// Only spawn for non-batch-mode agents
|
||||
// Only spawn for non-batch-mode agents (Aider, etc.)
|
||||
// Include active tab ID in session ID to match batch mode format
|
||||
const activeTabId = correctedSession.activeTabId || correctedSession.aiTabs?.[0]?.id || 'default';
|
||||
// Use agent.path (full path) if available for better cross-environment compatibility
|
||||
aiSpawnResult = await window.maestro.process.spawn({
|
||||
sessionId: `${correctedSession.id}-ai`,
|
||||
sessionId: `${correctedSession.id}-ai-${activeTabId}`,
|
||||
toolType: aiAgentType,
|
||||
cwd: correctedSession.cwd,
|
||||
command: agent.path || agent.command,
|
||||
@@ -444,7 +408,7 @@ export default function MaestroConsole() {
|
||||
isGitRepo, // Update Git status
|
||||
isLive: false, // Always start offline on app restart
|
||||
liveUrl: undefined, // Clear any stale URL
|
||||
aiLogs: correctedSession.aiLogs, // Preserve existing AI Terminal logs
|
||||
aiLogs: [], // Deprecated - logs are now in aiTabs
|
||||
shellLogs: correctedSession.shellLogs, // Preserve existing Command Terminal logs
|
||||
executionQueue: correctedSession.executionQueue || [], // Ensure backwards compatibility
|
||||
activeTimeMs: correctedSession.activeTimeMs || 0 // Ensure backwards compatibility
|
||||
@@ -583,7 +547,7 @@ export default function MaestroConsole() {
|
||||
// Set up process event listeners for real-time output
|
||||
useEffect(() => {
|
||||
// Handle process output data
|
||||
// sessionId will be in format: "{id}-ai-{tabId}", "{id}-ai" (legacy), "{id}-terminal", "{id}-batch-{timestamp}", etc.
|
||||
// sessionId will be in format: "{id}-ai-{tabId}", "{id}-terminal", "{id}-batch-{timestamp}", etc.
|
||||
const unsubscribeData = window.maestro.process.onData((sessionId: string, data: string) => {
|
||||
console.log('[onData] Received data for session:', sessionId, 'DataLen:', data.length, 'Preview:', data.substring(0, 200));
|
||||
|
||||
@@ -592,15 +556,12 @@ export default function MaestroConsole() {
|
||||
let isFromAi: boolean;
|
||||
let tabIdFromSession: string | undefined;
|
||||
|
||||
// Check for new format with tab ID: sessionId-ai-tabId
|
||||
// Format: sessionId-ai-tabId
|
||||
const aiTabMatch = sessionId.match(/^(.+)-ai-(.+)$/);
|
||||
if (aiTabMatch) {
|
||||
actualSessionId = aiTabMatch[1];
|
||||
tabIdFromSession = aiTabMatch[2];
|
||||
isFromAi = true;
|
||||
} else if (sessionId.endsWith('-ai')) {
|
||||
actualSessionId = sessionId.slice(0, -3); // Remove "-ai" suffix (legacy format)
|
||||
isFromAi = true;
|
||||
} else if (sessionId.endsWith('-terminal')) {
|
||||
// Ignore PTY terminal output - we use runCommand for terminal commands,
|
||||
// which emits data with plain session ID (not -terminal suffix)
|
||||
@@ -659,10 +620,9 @@ export default function MaestroConsole() {
|
||||
targetTab = getWriteModeTab(s) || getActiveTab(s);
|
||||
}
|
||||
if (!targetTab) {
|
||||
// Fallback: no tabs exist, use deprecated aiLogs (shouldn't happen normally)
|
||||
console.warn('[onData] No target tab found, falling back to aiLogs');
|
||||
const newLog: LogEntry = { id: generateId(), timestamp: Date.now(), source: 'stdout', text: data };
|
||||
return { ...s, aiLogs: [...s.aiLogs, newLog] };
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[onData] No target tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
const existingLogs = targetTab.logs;
|
||||
@@ -723,20 +683,17 @@ export default function MaestroConsole() {
|
||||
// Handle process exit
|
||||
const unsubscribeExit = window.maestro.process.onExit((sessionId: string, code: number) => {
|
||||
// Parse sessionId to determine which process exited
|
||||
// Format: {id}-ai-{tabId}, {id}-ai (legacy), {id}-terminal, {id}-batch-{timestamp}
|
||||
// Format: {id}-ai-{tabId}, {id}-terminal, {id}-batch-{timestamp}
|
||||
let actualSessionId: string;
|
||||
let isFromAi: boolean;
|
||||
let tabIdFromSession: string | undefined;
|
||||
|
||||
// Check for new format with tab ID: sessionId-ai-tabId
|
||||
// Format: sessionId-ai-tabId
|
||||
const aiTabMatch = sessionId.match(/^(.+)-ai-(.+)$/);
|
||||
if (aiTabMatch) {
|
||||
actualSessionId = aiTabMatch[1];
|
||||
tabIdFromSession = aiTabMatch[2];
|
||||
isFromAi = true;
|
||||
} else if (sessionId.endsWith('-ai')) {
|
||||
actualSessionId = sessionId.slice(0, -3);
|
||||
isFromAi = true;
|
||||
} else if (sessionId.endsWith('-terminal')) {
|
||||
actualSessionId = sessionId.slice(0, -9);
|
||||
isFromAi = false;
|
||||
@@ -785,7 +742,7 @@ export default function MaestroConsole() {
|
||||
const completedTab = tabIdFromSession
|
||||
? currentSession.aiTabs?.find(tab => tab.id === tabIdFromSession)
|
||||
: getActiveTab(currentSession);
|
||||
const logs = completedTab?.logs || currentSession.aiLogs;
|
||||
const logs = completedTab?.logs || [];
|
||||
const lastUserLog = logs.filter(log => log.source === 'user').pop();
|
||||
const lastAiLog = logs.filter(log => log.source === 'stdout' || log.source === 'ai').pop();
|
||||
const duration = currentSession.thinkingStartTime ? Date.now() - currentSession.thinkingStartTime : 0;
|
||||
@@ -1089,20 +1046,15 @@ export default function MaestroConsole() {
|
||||
}
|
||||
|
||||
// Parse sessionId to get actual session ID and tab ID
|
||||
// Format: ${sessionId}-ai-${tabId} or legacy ${sessionId}-ai
|
||||
// Format: ${sessionId}-ai-${tabId}
|
||||
let actualSessionId: string;
|
||||
let tabId: string | undefined;
|
||||
|
||||
// Check for new format with tab ID: sessionId-ai-tabId
|
||||
const aiTabMatch = sessionId.match(/^(.+)-ai-(.+)$/);
|
||||
if (aiTabMatch) {
|
||||
actualSessionId = aiTabMatch[1];
|
||||
tabId = aiTabMatch[2];
|
||||
console.log('[onSessionId] Parsed tab format - actualSessionId:', actualSessionId, 'tabId:', tabId);
|
||||
} else if (sessionId.endsWith('-ai')) {
|
||||
// Legacy format without tab ID
|
||||
actualSessionId = sessionId.slice(0, -3);
|
||||
console.log('[onSessionId] Parsed legacy format - actualSessionId:', actualSessionId);
|
||||
console.log('[onSessionId] Parsed - actualSessionId:', actualSessionId, 'tabId:', tabId);
|
||||
} else {
|
||||
actualSessionId = sessionId;
|
||||
console.log('[onSessionId] No format match - using as-is:', actualSessionId);
|
||||
@@ -1155,8 +1107,9 @@ export default function MaestroConsole() {
|
||||
}
|
||||
|
||||
if (!targetTab) {
|
||||
// Fallback: no tabs exist, use deprecated session-level field
|
||||
console.warn('[onSessionId] No target tab found, storing at session level (deprecated)');
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
// Still store at session-level for web API compatibility
|
||||
console.error('[onSessionId] No target tab found - session has no aiTabs, storing at session level only');
|
||||
return { ...s, claudeSessionId };
|
||||
}
|
||||
|
||||
@@ -1777,9 +1730,9 @@ export default function MaestroConsole() {
|
||||
: getActiveTab(s);
|
||||
|
||||
if (!targetTab) {
|
||||
// Fallback: no tabs exist, use deprecated aiLogs
|
||||
console.warn('[addLogToTab] No target tab found, using aiLogs (deprecated)');
|
||||
return { ...s, aiLogs: [...s.aiLogs, entry] };
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[addLogToTab] No target tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
// Update target tab's logs
|
||||
@@ -3131,9 +3084,10 @@ export default function MaestroConsole() {
|
||||
}, [shortcutsHelpOpen]);
|
||||
|
||||
// Auto-scroll logs
|
||||
const activeTabLogs = activeSession ? getActiveTab(activeSession)?.logs : undefined;
|
||||
useEffect(() => {
|
||||
logsEndRef.current?.scrollIntoView({ behavior: 'instant' });
|
||||
}, [activeSession?.aiLogs, activeSession?.shellLogs, activeSession?.inputMode]);
|
||||
}, [activeTabLogs, activeSession?.shellLogs, activeSession?.inputMode]);
|
||||
|
||||
// --- ACTIONS ---
|
||||
const cycleSession = (dir: 'next' | 'prev') => {
|
||||
@@ -3324,7 +3278,7 @@ export default function MaestroConsole() {
|
||||
cwd: workingDir,
|
||||
fullPath: workingDir,
|
||||
isGitRepo,
|
||||
aiLogs: [{ id: generateId(), timestamp: Date.now(), source: 'system', text: `${name} ready.` }],
|
||||
aiLogs: [], // Deprecated - logs are now in aiTabs
|
||||
shellLogs: [{ id: generateId(), timestamp: Date.now(), source: 'system', text: 'Shell Session Ready.' }],
|
||||
workLog: [],
|
||||
scratchPadContent: '',
|
||||
@@ -3613,18 +3567,27 @@ export default function MaestroConsole() {
|
||||
if (existingCommand) {
|
||||
// Command exists but not available in this mode - show error and don't send to AI
|
||||
const modeLabel = isTerminalMode ? 'AI' : 'terminal';
|
||||
const targetLogKey = activeSession.inputMode === 'ai' ? 'aiLogs' : 'shellLogs';
|
||||
const errorLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `${commandText} is only available in ${modeLabel} mode.`
|
||||
};
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
return {
|
||||
...s,
|
||||
[targetLogKey]: [...s[targetLogKey], {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `${commandText} is only available in ${modeLabel} mode.`
|
||||
}]
|
||||
};
|
||||
if (activeSession.inputMode === 'ai') {
|
||||
// Add to active tab's logs
|
||||
const activeTab = getActiveTab(s);
|
||||
if (!activeTab) return s;
|
||||
return {
|
||||
...s,
|
||||
aiTabs: s.aiTabs.map(tab =>
|
||||
tab.id === activeTab.id ? { ...tab, logs: [...tab.logs, errorLog] } : tab
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return { ...s, shellLogs: [...s.shellLogs, errorLog] };
|
||||
}
|
||||
}));
|
||||
setInputValue('');
|
||||
setSlashCommandOpen(false);
|
||||
@@ -3728,7 +3691,6 @@ export default function MaestroConsole() {
|
||||
}
|
||||
|
||||
const currentMode = activeSession.inputMode;
|
||||
const targetLogKey = currentMode === 'ai' ? 'aiLogs' : 'shellLogs';
|
||||
|
||||
// Queue messages when AI is busy (only in AI mode)
|
||||
// For read-only mode tabs: only queue if THIS TAB is busy (allows parallel execution)
|
||||
@@ -3874,22 +3836,12 @@ export default function MaestroConsole() {
|
||||
};
|
||||
}
|
||||
|
||||
// For AI mode, add to ACTIVE TAB's logs (not session.aiLogs)
|
||||
// For AI mode, add to ACTIVE TAB's logs
|
||||
const activeTab = getActiveTab(s);
|
||||
if (!activeTab) {
|
||||
// Fallback: no tabs exist, use deprecated aiLogs
|
||||
console.warn('[processInput] No active tab found, using aiLogs (deprecated)');
|
||||
return {
|
||||
...s,
|
||||
aiLogs: [...s.aiLogs, newEntry],
|
||||
state: 'busy',
|
||||
busySource: currentMode,
|
||||
thinkingStartTime: Date.now(),
|
||||
currentCycleTokens: 0,
|
||||
contextUsage: Math.min(s.contextUsage + 5, 100),
|
||||
shellCwd: newShellCwd,
|
||||
[historyKey]: newHistory
|
||||
};
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[processInput] No active tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
// Update the active tab's logs and state to 'busy' for write-mode tracking
|
||||
@@ -4014,23 +3966,25 @@ export default function MaestroConsole() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to spawn Claude batch process:', error);
|
||||
const errorLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to spawn Claude process - ${error.message}`
|
||||
};
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
// Reset active tab's state to 'idle' for write-mode tracking
|
||||
// Reset active tab's state to 'idle' and add error log
|
||||
const updatedAiTabs = s.aiTabs?.length > 0
|
||||
? s.aiTabs.map(tab =>
|
||||
tab.id === s.activeTabId ? { ...tab, state: 'idle' as const } : tab
|
||||
tab.id === s.activeTabId
|
||||
? { ...tab, state: 'idle' as const, logs: [...tab.logs, errorLog] }
|
||||
: tab
|
||||
)
|
||||
: s.aiTabs;
|
||||
return {
|
||||
...s,
|
||||
state: 'idle',
|
||||
[targetLogKey]: [...s[targetLogKey], {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to spawn Claude process - ${error.message}`
|
||||
}],
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
}));
|
||||
@@ -4070,23 +4024,25 @@ export default function MaestroConsole() {
|
||||
// AI mode: Write to stdin
|
||||
window.maestro.process.write(targetSessionId, capturedInputValue).catch(error => {
|
||||
console.error('Failed to write to process:', error);
|
||||
const errorLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to write to process - ${error.message}`
|
||||
};
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
// Reset active tab's state to 'idle' for write-mode tracking (if tabs exist)
|
||||
// Reset active tab's state to 'idle' and add error log
|
||||
const updatedAiTabs = s.aiTabs?.length > 0
|
||||
? s.aiTabs.map(tab =>
|
||||
tab.id === s.activeTabId ? { ...tab, state: 'idle' as const } : tab
|
||||
tab.id === s.activeTabId
|
||||
? { ...tab, state: 'idle' as const, logs: [...tab.logs, errorLog] }
|
||||
: tab
|
||||
)
|
||||
: s.aiTabs;
|
||||
return {
|
||||
...s,
|
||||
state: 'idle',
|
||||
[targetLogKey]: [...s[targetLogKey], {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to write to process - ${error.message}`
|
||||
}],
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
}));
|
||||
@@ -4341,21 +4297,10 @@ export default function MaestroConsole() {
|
||||
)
|
||||
: s.aiTabs;
|
||||
|
||||
// Fallback: if no active tab, use deprecated aiLogs
|
||||
if (!activeTab) {
|
||||
return {
|
||||
...s,
|
||||
state: 'busy' as SessionState,
|
||||
busySource: 'ai',
|
||||
thinkingStartTime: Date.now(),
|
||||
currentCycleTokens: 0,
|
||||
currentCycleBytes: 0,
|
||||
aiLogs: [...s.aiLogs, userLogEntry],
|
||||
...(commandMetadata && {
|
||||
aiCommandHistory: Array.from(new Set([...(s.aiCommandHistory || []), command.trim()])).slice(-50)
|
||||
}),
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[runAICommand] No active tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -4404,15 +4349,10 @@ export default function MaestroConsole() {
|
||||
)
|
||||
: s.aiTabs;
|
||||
|
||||
// Fallback: if no active tab, use deprecated aiLogs
|
||||
if (!activeTab) {
|
||||
return {
|
||||
...s,
|
||||
state: 'idle' as SessionState,
|
||||
busySource: undefined,
|
||||
aiLogs: [...s.aiLogs, errorLogEntry],
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[runAICommand error] No active tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -4567,14 +4507,10 @@ export default function MaestroConsole() {
|
||||
)
|
||||
: s.aiTabs;
|
||||
|
||||
// Fallback: if no active tab, use deprecated aiLogs
|
||||
if (!activeTab) {
|
||||
return {
|
||||
...s,
|
||||
state: 'idle',
|
||||
aiLogs: [...s.aiLogs, errorLogEntry],
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
// No tabs exist - this is a bug, sessions must have aiTabs
|
||||
console.error('[processQueuedItem error] No active tab found - session has no aiTabs, this should not happen');
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -4593,8 +4529,10 @@ export default function MaestroConsole() {
|
||||
if (!activeSession) return;
|
||||
|
||||
const currentMode = activeSession.inputMode;
|
||||
const targetSessionId = currentMode === 'ai' ? `${activeSession.id}-ai` : `${activeSession.id}-terminal`;
|
||||
const targetLogKey = currentMode === 'ai' ? 'aiLogs' : 'shellLogs';
|
||||
const activeTab = getActiveTab(activeSession);
|
||||
const targetSessionId = currentMode === 'ai'
|
||||
? `${activeSession.id}-ai-${activeTab?.id || 'default'}`
|
||||
: `${activeSession.id}-terminal`;
|
||||
|
||||
try {
|
||||
// Send interrupt signal (Ctrl+C)
|
||||
@@ -4621,33 +4559,49 @@ export default function MaestroConsole() {
|
||||
try {
|
||||
await window.maestro.process.kill(targetSessionId);
|
||||
|
||||
const killLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: 'Process forcefully terminated'
|
||||
};
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSession.id) return s;
|
||||
return {
|
||||
...s,
|
||||
[targetLogKey]: [...s[targetLogKey], {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: 'Process forcefully terminated'
|
||||
}],
|
||||
state: 'idle'
|
||||
};
|
||||
if (currentMode === 'ai') {
|
||||
const tab = getActiveTab(s);
|
||||
if (!tab) return { ...s, state: 'idle' };
|
||||
return {
|
||||
...s,
|
||||
state: 'idle',
|
||||
aiTabs: s.aiTabs.map(t =>
|
||||
t.id === tab.id ? { ...t, logs: [...t.logs, killLog] } : t
|
||||
)
|
||||
};
|
||||
}
|
||||
return { ...s, shellLogs: [...s.shellLogs, killLog], state: 'idle' };
|
||||
}));
|
||||
} catch (killError) {
|
||||
console.error('Failed to kill process:', killError);
|
||||
const errorLog: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to terminate process - ${killError.message}`
|
||||
};
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSession.id) return s;
|
||||
return {
|
||||
...s,
|
||||
[targetLogKey]: [...s[targetLogKey], {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to terminate process - ${killError.message}`
|
||||
}],
|
||||
state: 'idle'
|
||||
};
|
||||
if (currentMode === 'ai') {
|
||||
const tab = getActiveTab(s);
|
||||
if (!tab) return { ...s, state: 'idle' };
|
||||
return {
|
||||
...s,
|
||||
state: 'idle',
|
||||
aiTabs: s.aiTabs.map(t =>
|
||||
t.id === tab.id ? { ...t, logs: [...t.logs, errorLog] } : t
|
||||
)
|
||||
};
|
||||
}
|
||||
return { ...s, shellLogs: [...s.shellLogs, errorLog], state: 'idle' };
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,8 +229,8 @@ export function MainPanel(props: MainPanelProps) {
|
||||
};
|
||||
|
||||
fetchGitInfo();
|
||||
// Refresh git info every 10 seconds
|
||||
const interval = setInterval(fetchGitInfo, 10000);
|
||||
// Refresh git info every 30 seconds (reduced from 10s for performance)
|
||||
const interval = setInterval(fetchGitInfo, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [activeSession?.id, activeSession?.isGitRepo, activeSession?.cwd, activeSession?.shellCwd, activeSession?.inputMode]);
|
||||
|
||||
|
||||
@@ -199,15 +199,24 @@ export function SessionList(props: SessionListProps) {
|
||||
const gitSessions = sessions.filter(s => s.isGitRepo);
|
||||
if (gitSessions.length === 0) return;
|
||||
|
||||
const newCounts = new Map<string, number>();
|
||||
// Parallelize git status calls for better performance
|
||||
// Sequential calls with 10 sessions = 1-2s, parallel = 200-300ms
|
||||
const results = await Promise.all(
|
||||
gitSessions.map(async (session) => {
|
||||
try {
|
||||
const cwd = session.inputMode === 'terminal' ? (session.shellCwd || session.cwd) : session.cwd;
|
||||
const status = await gitService.getStatus(cwd);
|
||||
return [session.id, status.files.length] as const;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const session of gitSessions) {
|
||||
try {
|
||||
const cwd = session.inputMode === 'terminal' ? (session.shellCwd || session.cwd) : session.cwd;
|
||||
const status = await gitService.getStatus(cwd);
|
||||
newCounts.set(session.id, status.files.length);
|
||||
} catch (error) {
|
||||
// Ignore errors, don't show indicator if we can't get status
|
||||
const newCounts = new Map<string, number>();
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
newCounts.set(result[0], result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -564,7 +564,7 @@ export function TabBar({
|
||||
return (
|
||||
<div
|
||||
ref={tabBarRef}
|
||||
className="flex items-end gap-0.5 px-2 pt-2 border-b overflow-x-auto scrollbar-thin"
|
||||
className="flex items-end gap-0.5 px-2 pt-2 border-b overflow-x-auto overflow-y-hidden scrollbar-thin"
|
||||
style={{
|
||||
backgroundColor: theme.colors.bgSidebar,
|
||||
borderColor: theme.colors.border
|
||||
|
||||
@@ -329,7 +329,7 @@ const LogItemComponent = memo(({
|
||||
style={{ fontFamily, color: theme.colors.textDim, opacity: 0.6 }}>
|
||||
{new Date(log.timestamp).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
|
||||
</div>
|
||||
<div className={`flex-1 p-4 rounded-xl border ${isUserMessage ? 'rounded-tr-none' : 'rounded-tl-none'} relative`}
|
||||
<div className={`flex-1 p-4 ${isUserMessage && log.readOnly ? 'pt-8' : ''} rounded-xl border ${isUserMessage ? 'rounded-tr-none' : 'rounded-tl-none'} relative`}
|
||||
style={{
|
||||
backgroundColor: isUserMessage
|
||||
? isAIMode
|
||||
@@ -1044,11 +1044,10 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// In AI mode, use the active tab's logs if tabs exist, otherwise fall back to session.aiLogs
|
||||
// This supports the new multi-tab feature while maintaining backwards compatibility
|
||||
// In AI mode, use the active tab's logs
|
||||
const activeTab = session.inputMode === 'ai' ? getActiveTab(session) : undefined;
|
||||
const activeLogs: LogEntry[] = session.inputMode === 'ai'
|
||||
? (activeTab?.logs ?? session.aiLogs)
|
||||
? (activeTab?.logs ?? [])
|
||||
: session.shellLogs;
|
||||
|
||||
// In AI mode, collapse consecutive non-user entries into single response blocks
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { Session, Group, ToolType, LogEntry, AITab } from '../types';
|
||||
import { generateId } from '../utils/ids';
|
||||
import type { Session, Group } from '../types';
|
||||
import { gitService } from '../services/git';
|
||||
|
||||
// Maximum number of log entries to persist per AI tab
|
||||
@@ -18,40 +17,14 @@ const compareNamesIgnoringEmojis = (a: string, b: string): number => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrate a session from old format (without aiTabs) to new format.
|
||||
* Creates a single tab from the legacy claudeSessionId, aiLogs, etc.
|
||||
* This is a basic migration; starred/named status can be looked up later in restoreSession.
|
||||
* Prepare a session for loading by resetting runtime-only fields.
|
||||
*/
|
||||
const migrateSessionToTabFormat = (session: Session): Session => {
|
||||
// If session already has aiTabs, just ensure closedTabHistory is initialized
|
||||
if (session.aiTabs && session.aiTabs.length > 0) {
|
||||
return {
|
||||
...session,
|
||||
// closedTabHistory is runtime-only and should not be persisted
|
||||
// Always reset to empty array on load
|
||||
closedTabHistory: []
|
||||
};
|
||||
}
|
||||
|
||||
// Create initial tab from legacy data
|
||||
const initialTab: AITab = {
|
||||
id: generateId(),
|
||||
claudeSessionId: session.claudeSessionId || null,
|
||||
name: null, // Name will be looked up in restoreSession if needed
|
||||
starred: false, // Starred will be looked up in restoreSession if needed
|
||||
logs: session.aiLogs || [],
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
usageStats: session.usageStats,
|
||||
createdAt: Date.now(),
|
||||
state: 'idle'
|
||||
};
|
||||
|
||||
const prepareSessionForLoad = (session: Session): Session => {
|
||||
return {
|
||||
...session,
|
||||
aiTabs: [initialTab],
|
||||
activeTabId: initialTab.id,
|
||||
closedTabHistory: [] // Runtime-only, always empty on load
|
||||
// closedTabHistory is runtime-only and should not be persisted
|
||||
// Always reset to empty array on load
|
||||
closedTabHistory: []
|
||||
};
|
||||
};
|
||||
|
||||
@@ -141,13 +114,11 @@ export function useSessionManager(): UseSessionManagerReturn {
|
||||
|
||||
// Handle sessions
|
||||
if (savedSessions && savedSessions.length > 0) {
|
||||
// Check Git repository status and migrate to aiTabs format for all loaded sessions
|
||||
// Check Git repository status and prepare sessions for load
|
||||
const sessionsWithGitStatus = await Promise.all(
|
||||
savedSessions.map(async (session) => {
|
||||
const isGitRepo = await gitService.isRepo(session.cwd);
|
||||
// Migrate to aiTabs format and ensure closedTabHistory is reset
|
||||
const migratedSession = migrateSessionToTabFormat({ ...session, isGitRepo });
|
||||
return migratedSession;
|
||||
return prepareSessionForLoad({ ...session, isGitRepo });
|
||||
})
|
||||
);
|
||||
setSessions(sessionsWithGitStatus);
|
||||
|
||||
Reference in New Issue
Block a user