From 1ac28555f758f01250120b9e8ea22bea8c7075a0 Mon Sep 17 00:00:00 2001 From: chr1syy Date: Fri, 13 Mar 2026 18:19:39 +0100 Subject: [PATCH 1/5] feat: add SQLite session reading support for OpenCode v1.2+ Co-Authored-By: Claude Opus 4.6 --- src/main/storage/opencode-session-storage.ts | 560 ++++++++++++++++++- 1 file changed, 531 insertions(+), 29 deletions(-) diff --git a/src/main/storage/opencode-session-storage.ts b/src/main/storage/opencode-session-storage.ts index fb5c4fe67b..e8f8e1c555 100644 --- a/src/main/storage/opencode-session-storage.ts +++ b/src/main/storage/opencode-session-storage.ts @@ -2,13 +2,12 @@ * OpenCode Session Storage Implementation * * This module implements the AgentSessionStorage interface for OpenCode. - * OpenCode stores sessions as JSON files in ~/.local/share/opencode/storage/ * - * Directory structure: - * - project/ - Project metadata (SHA1 hash of path as ID) - * - session/{projectID}/ - Session metadata per project - * - message/{sessionID}/ - Messages per session - * - part/{messageID}/ - Message parts (text, tool, reasoning) + * OpenCode v1.2+ stores sessions in SQLite at ~/.local/share/opencode/opencode.db + * Older versions used JSON files at ~/.local/share/opencode/storage/ + * + * This implementation reads from SQLite first, falls back to JSON for pre-v1.2 + * installs, and deduplicates sessions when both sources exist (migration period). * * Session IDs: Format is `ses_{base62}` (e.g., ses_4d585107dffeO9bO3HvMdvLYyC) * Project IDs: SHA1 hash of the project path @@ -21,7 +20,9 @@ import path from 'path'; import os from 'os'; import fs from 'fs/promises'; +import fsSync from 'fs'; import { createHash } from 'crypto'; +import Database from 'better-sqlite3'; import { logger } from '../utils/logger'; import { captureException } from '../utils/sentry'; import { readFileRemote, readDirRemote, statRemote } from '../utils/remote-fs'; @@ -38,19 +39,34 @@ import { isWindows } from '../../shared/platformDetection'; const LOG_CONTEXT = '[OpenCodeSessionStorage]'; /** - * Get OpenCode storage base directory (platform-specific) - * - Linux/macOS: ~/.local/share/opencode/storage - * - Windows: %APPDATA%\opencode\storage + * Get OpenCode data base directory (platform-specific) + * - Linux/macOS: ~/.local/share/opencode + * - Windows: %APPDATA%\opencode */ -function getOpenCodeStorageDir(): string { +function getOpenCodeDataDir(): string { if (isWindows()) { const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); - return path.join(appData, 'opencode', 'storage'); + return path.join(appData, 'opencode'); } - return path.join(os.homedir(), '.local', 'share', 'opencode', 'storage'); + return path.join(os.homedir(), '.local', 'share', 'opencode'); +} + +/** + * Get OpenCode JSON storage directory (pre-v1.2) + */ +function getOpenCodeStorageDir(): string { + return path.join(getOpenCodeDataDir(), 'storage'); +} + +/** + * Get OpenCode SQLite database path (v1.2+) + */ +function getOpenCodeDbPath(): string { + return path.join(getOpenCodeDataDir(), 'opencode.db'); } const OPENCODE_STORAGE_DIR = getOpenCodeStorageDir(); +const OPENCODE_DB_PATH = getOpenCodeDbPath(); /** * OpenCode project metadata structure @@ -133,6 +149,113 @@ interface OpenCodePart { }; } +// ─── SQLite row types (v1.2+) ──────────────────────────────────────────────── + +/** + * Raw row from the SQLite `session` table + */ +interface SqliteSessionRow { + id: string; + project_id: string; + directory: string; + title: string; + version: string; + time_created: number; // Unix ms + time_updated: number; // Unix ms + summary_additions: number | null; + summary_deletions: number | null; + summary_files: number | null; +} + +/** + * Raw row from the SQLite `message` table + * The `data` column is a JSON blob containing role, model, tokens, cost, etc. + */ +interface SqliteMessageRow { + id: string; + session_id: string; + time_created: number; + time_updated: number; + data: string; // JSON blob +} + +/** + * Parsed message data from the SQLite `data` JSON blob + */ +interface SqliteMessageData { + role?: 'user' | 'assistant'; + modelID?: string; + providerID?: string; + agent?: string; + tokens?: { + input?: number; + output?: number; + reasoning?: number; + cache?: { + read?: number; + write?: number; + }; + }; + cost?: number; +} + +/** + * Parsed part data from the SQLite `data` JSON blob + */ +interface SqlitePartData { + type?: 'text' | 'reasoning' | 'tool' | 'step-start' | 'step-finish'; + text?: string; + tool?: string; + state?: { + status?: string; + input?: unknown; + output?: unknown; + }; +} + +// ─── SQLite helpers ────────────────────────────────────────────────────────── + +/** + * Open the OpenCode SQLite database in read-only mode. + * Returns null if the database file doesn't exist. + */ +function openOpenCodeDb(dbPath: string = OPENCODE_DB_PATH): Database.Database | null { + try { + if (!fsSync.existsSync(dbPath)) { + return null; + } + const db = new Database(dbPath, { readonly: true, fileMustExist: true }); + db.pragma('journal_mode = WAL'); + return db; + } catch (error) { + logger.warn(`Failed to open OpenCode SQLite database: ${error}`, LOG_CONTEXT); + return null; + } +} + +/** + * Safely parse a JSON string, returning null on failure + */ +function safeJsonParse(json: string): T | null { + try { + return JSON.parse(json) as T; + } catch { + return null; + } +} + +/** + * Check if a table exists in a SQLite database + */ +function tableExists(db: Database.Database, tableName: string): boolean { + const row = db + .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?") + .get(tableName) as { name: string } | undefined; + return !!row; +} + +// ─── Shared helpers ────────────────────────────────────────────────────────── + /** * Generate the project ID hash from a path (SHA1) */ @@ -202,7 +325,8 @@ async function listJsonFilesRemote(dirPath: string, sshConfig: SshRemoteConfig): /** * OpenCode Session Storage Implementation * - * Provides access to OpenCode's local session storage at ~/.local/share/opencode/storage/ + * Reads from SQLite (v1.2+) with JSON file fallback (pre-v1.2). + * During migration periods, both sources are merged with dedup by session ID. */ export class OpenCodeSessionStorage extends BaseSessionStorage { readonly agentId: ToolType = 'opencode'; @@ -610,19 +734,353 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { return textParts.join(' ').trim(); } + // ─── SQLite-based methods (OpenCode v1.2+) ────────────────────────────── + + /** + * List sessions from SQLite database for a given project path. + * Returns null if the database doesn't exist or lacks the expected schema. + */ + private listSessionsSqlite(projectPath: string): AgentSessionInfo[] | null { + const db = openOpenCodeDb(); + if (!db) return null; + + try { + if (!tableExists(db, 'session') || !tableExists(db, 'project')) { + return null; + } + + const normalizedPath = path.resolve(projectPath).replace(/\/+$/, ''); + + // Find matching project(s) — exact match or subdirectory match + const projects = db.prepare('SELECT id, worktree FROM project').all() as Array<{ + id: string; + worktree: string; + }>; + + const matchingProjectIds: string[] = []; + let hasGlobalProject = false; + for (const proj of projects) { + // Skip the 'global' project (worktree '/') from project-level matching — + // it matches everything. Its sessions are filtered by directory below. + if (proj.id === 'global') { + hasGlobalProject = true; + continue; + } + const storedPath = path.resolve(proj.worktree).replace(/\/+$/, ''); + if ( + storedPath === normalizedPath || + normalizedPath.startsWith(storedPath + '/') || + storedPath.startsWith(normalizedPath + '/') + ) { + matchingProjectIds.push(proj.id); + } + } + + // Collect sessions from matching dedicated projects + let sessions: SqliteSessionRow[] = []; + if (matchingProjectIds.length > 0) { + const placeholders = matchingProjectIds.map(() => '?').join(','); + sessions = db + .prepare( + `SELECT id, project_id, directory, title, version, time_created, time_updated, summary_additions, summary_deletions, summary_files FROM session WHERE project_id IN (${placeholders}) ORDER BY time_updated DESC` + ) + .all(...matchingProjectIds) as SqliteSessionRow[]; + } + + // Also include global project sessions that match by directory field + if (hasGlobalProject) { + const globalSessions = db + .prepare( + "SELECT id, project_id, directory, title, version, time_created, time_updated, summary_additions, summary_deletions, summary_files FROM session WHERE project_id = 'global' AND (directory = ? OR directory LIKE ?) ORDER BY time_updated DESC" + ) + .all(normalizedPath, normalizedPath + '/%') as SqliteSessionRow[]; + if (globalSessions.length > 0) { + const existingIds = new Set(sessions.map((s) => s.id)); + for (const gs of globalSessions) { + if (!existingIds.has(gs.id)) { + sessions.push(gs); + } + } + } + } + + if (sessions.length === 0) { + logger.info(`No OpenCode sessions found in SQLite for: ${normalizedPath}`, LOG_CONTEXT); + return []; + } + + logger.info( + `Found ${sessions.length} OpenCode sessions in SQLite for: ${normalizedPath}`, + LOG_CONTEXT + ); + + return this.convertSqliteSessionRows(sessions, projectPath, db); + } catch (error) { + logger.warn(`Error reading OpenCode SQLite database: ${error}`, LOG_CONTEXT); + return null; + } finally { + db.close(); + } + } + + /** + * Convert SQLite session rows to AgentSessionInfo array, loading message stats + */ + private convertSqliteSessionRows( + rows: SqliteSessionRow[], + projectPath: string, + db: Database.Database + ): AgentSessionInfo[] { + const hasMessageTable = tableExists(db, 'message'); + const hasPartTable = tableExists(db, 'part'); + const sessions: AgentSessionInfo[] = []; + + for (const row of rows) { + let messageCount = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalCacheWriteTokens = 0; + let totalCost = 0; + let firstMessage = row.title || ''; + let durationSeconds = 0; + + if (hasMessageTable) { + const messages = db + .prepare( + 'SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC' + ) + .all(row.id) as Array<{ id: string; data: string; time_created: number }>; + + messageCount = messages.length; + + if (messages.length >= 2) { + const first = messages[0].time_created; + const last = messages[messages.length - 1].time_created; + if (first && last) { + durationSeconds = Math.max(0, Math.floor((last - first) / 1000)); + } + } + + // Aggregate stats and find preview message + let foundPreview = false; + for (const msg of messages) { + const data = safeJsonParse(msg.data); + if (!data) continue; + + if (data.tokens) { + totalInputTokens += data.tokens.input || 0; + totalOutputTokens += data.tokens.output || 0; + totalCacheReadTokens += data.tokens.cache?.read || 0; + totalCacheWriteTokens += data.tokens.cache?.write || 0; + } + if (data.cost) { + totalCost += data.cost; + } + + // Get preview from first assistant message with text + if (!foundPreview && data.role === 'assistant' && hasPartTable) { + const parts = db + .prepare('SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC') + .all(msg.id) as Array<{ data: string }>; + for (const part of parts) { + const partData = safeJsonParse(part.data); + if (partData?.type === 'text' && partData.text?.trim()) { + firstMessage = partData.text; + foundPreview = true; + break; + } + } + } + + // Fall back to first user message if no assistant text found + if (!foundPreview && data.role === 'user' && hasPartTable) { + const parts = db + .prepare('SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC') + .all(msg.id) as Array<{ data: string }>; + for (const part of parts) { + const partData = safeJsonParse(part.data); + if (partData?.type === 'text' && partData.text?.trim()) { + firstMessage = partData.text; + break; + } + } + } + } + } + + const createdAt = row.time_created + ? new Date(row.time_created).toISOString() + : new Date().toISOString(); + const updatedAt = row.time_updated ? new Date(row.time_updated).toISOString() : createdAt; + + sessions.push({ + sessionId: row.id, + projectPath, + timestamp: createdAt, + modifiedAt: updatedAt, + firstMessage: firstMessage.slice(0, 200), + messageCount, + sizeBytes: 0, + costUsd: totalCost, + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheCreationTokens: totalCacheWriteTokens, + durationSeconds, + }); + } + + return sessions; + } + + /** + * Load messages for a session from SQLite. + * Returns null if the database doesn't exist or lacks the expected schema. + */ + private loadSessionMessagesSqlite(sessionId: string): { + messages: OpenCodeMessage[]; + parts: Map; + totalInputTokens: number; + totalOutputTokens: number; + totalCacheReadTokens: number; + totalCacheWriteTokens: number; + totalCost: number; + } | null { + const db = openOpenCodeDb(); + if (!db) return null; + + try { + if (!tableExists(db, 'message')) return null; + + const messageRows = db + .prepare( + 'SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? ORDER BY time_created ASC' + ) + .all(sessionId) as SqliteMessageRow[]; + + if (messageRows.length === 0) return null; + + const hasPartTable = tableExists(db, 'part'); + const messages: OpenCodeMessage[] = []; + const parts = new Map(); + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalCacheWriteTokens = 0; + let totalCost = 0; + + for (const row of messageRows) { + const data = safeJsonParse(row.data); + if (!data) continue; + + const msg: OpenCodeMessage = { + id: row.id, + sessionID: sessionId, + role: data.role || 'user', + time: { created: row.time_created }, + tokens: data.tokens, + cost: data.cost, + }; + messages.push(msg); + + if (data.tokens) { + totalInputTokens += data.tokens.input || 0; + totalOutputTokens += data.tokens.output || 0; + totalCacheReadTokens += data.tokens.cache?.read || 0; + totalCacheWriteTokens += data.tokens.cache?.write || 0; + } + if (data.cost) { + totalCost += data.cost; + } + + // Load parts from SQLite + if (hasPartTable) { + const partRows = db + .prepare('SELECT id, data FROM part WHERE message_id = ? ORDER BY time_created ASC') + .all(row.id) as Array<{ id: string; data: string }>; + + const messageParts: OpenCodePart[] = []; + for (const partRow of partRows) { + const partData = safeJsonParse(partRow.data); + if (partData) { + messageParts.push({ + id: partRow.id, + messageID: row.id, + type: partData.type || 'text', + text: partData.text, + tool: partData.tool, + state: partData.state, + }); + } + } + parts.set(row.id, messageParts); + } + } + + return { + messages, + parts, + totalInputTokens, + totalOutputTokens, + totalCacheReadTokens, + totalCacheWriteTokens, + totalCost, + }; + } catch (error) { + logger.warn(`Error loading messages from OpenCode SQLite: ${error}`, LOG_CONTEXT); + return null; + } finally { + db.close(); + } + } + + // ─── Merged listing (SQLite + JSON) ───────────────────────────────────── + async listSessions( projectPath: string, sshConfig?: SshRemoteConfig ): Promise { - // Use SSH remote access if config provided + // Use SSH remote access if config provided (JSON only for SSH — no remote SQLite) if (sshConfig) { return this.listSessionsRemote(projectPath, sshConfig); } + // Try SQLite first (v1.2+), then fall back to JSON, merge and dedup + const sqliteSessions = this.listSessionsSqlite(projectPath); + const jsonSessions = await this.listSessionsJson(projectPath); + + if (sqliteSessions && sqliteSessions.length > 0) { + if (jsonSessions.length > 0) { + // Merge: SQLite is authoritative, add JSON-only sessions + const sqliteIds = new Set(sqliteSessions.map((s) => s.sessionId)); + const merged = [...sqliteSessions]; + for (const jsonSession of jsonSessions) { + if (!sqliteIds.has(jsonSession.sessionId)) { + merged.push(jsonSession); + } + } + merged.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime()); + logger.info( + `Merged ${sqliteSessions.length} SQLite + ${merged.length - sqliteSessions.length} JSON-only sessions for: ${projectPath}`, + LOG_CONTEXT + ); + return merged; + } + return sqliteSessions; + } + + // SQLite unavailable or empty — use JSON results + return jsonSessions; + } + + /** + * List sessions from JSON files (pre-v1.2 format) + */ + private async listSessionsJson(projectPath: string): Promise { const projectId = await this.findProjectId(projectPath); if (!projectId) { - logger.info(`No OpenCode project found for path: ${projectPath}`, LOG_CONTEXT); return []; } @@ -634,7 +1092,6 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { try { await fs.access(sessionDir); } catch { - logger.info(`No OpenCode sessions directory for project: ${projectPath}`, LOG_CONTEXT); return []; } @@ -731,10 +1188,12 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { // Sort by modified date (newest first) sessions.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime()); - logger.info( - `Found ${sessions.length} OpenCode sessions for project: ${projectPath}`, - LOG_CONTEXT - ); + if (sessions.length > 0) { + logger.info( + `Found ${sessions.length} OpenCode sessions (JSON) for: ${projectPath}`, + LOG_CONTEXT + ); + } return sessions; } @@ -872,9 +1331,22 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { options?: SessionReadOptions, sshConfig?: SshRemoteConfig ): Promise { - const { messages, parts } = sshConfig - ? await this.loadSessionMessagesRemote(sessionId, sshConfig) - : await this.loadSessionMessages(sessionId); + // Try SQLite first for local sessions, fall back to JSON + let loaded: { + messages: OpenCodeMessage[]; + parts: Map; + } | null = null; + + if (sshConfig) { + loaded = await this.loadSessionMessagesRemote(sessionId, sshConfig); + } else { + loaded = this.loadSessionMessagesSqlite(sessionId); + if (!loaded) { + loaded = await this.loadSessionMessages(sessionId); + } + } + + const { messages, parts } = loaded; const sessionMessages: SessionMessage[] = []; @@ -911,9 +1383,22 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { _projectPath: string, sshConfig?: SshRemoteConfig ): Promise { - const { messages, parts } = sshConfig - ? await this.loadSessionMessagesRemote(sessionId, sshConfig) - : await this.loadSessionMessages(sessionId); + // Try SQLite first for local sessions, fall back to JSON + let loaded: { + messages: OpenCodeMessage[]; + parts: Map; + } | null = null; + + if (sshConfig) { + loaded = await this.loadSessionMessagesRemote(sessionId, sshConfig); + } else { + loaded = this.loadSessionMessagesSqlite(sessionId); + if (!loaded) { + loaded = await this.loadSessionMessages(sessionId); + } + } + + const { messages, parts } = loaded; return messages .filter((msg) => msg.role === 'user' || msg.role === 'assistant') @@ -929,11 +1414,14 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { sessionId: string, sshConfig?: SshRemoteConfig ): string | null { - // OpenCode uses a more complex structure with multiple directories - // Return the message directory as the "session path" if (sshConfig) { return this.getRemoteMessageDir(sessionId); } + // For SQLite-backed sessions, return the database path + if (fsSync.existsSync(OPENCODE_DB_PATH)) { + return OPENCODE_DB_PATH; + } + // Fallback to JSON message directory return this.getMessageDir(sessionId); } @@ -951,7 +1439,21 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { } try { - // Load all messages for the session + // Check if this session exists in SQLite — deletion not supported for SQLite sessions + // (we open the DB read-only and shouldn't modify OpenCode's database) + const sqliteResult = this.loadSessionMessagesSqlite(sessionId); + if (sqliteResult && sqliteResult.messages.length > 0) { + logger.warn( + 'Delete message pair not supported for SQLite-backed OpenCode sessions', + LOG_CONTEXT + ); + return { + success: false, + error: 'Delete not supported for OpenCode v1.2+ SQLite sessions', + }; + } + + // Load all messages for the session (JSON files) const { messages, parts } = await this.loadSessionMessages(sessionId); if (messages.length === 0) { From 7c0bff4ff6fada803c56c3f8914d3f164471e41b Mon Sep 17 00:00:00 2001 From: chr1syy Date: Fri, 13 Mar 2026 20:20:40 +0100 Subject: [PATCH 2/5] fix: address PR review comments for SQLite session storage - Remove WAL pragma on readonly connection (was silently breaking all SQLite reads) - Set foundPreview=true for user message fallback to prevent overwrite - Escape LIKE wildcards in directory path queries - Return empty result instead of null for sessions with no messages - Check session existence in SQLite before returning DB path in getSessionPath - Fix prettier formatting in docs/releases.md Co-Authored-By: Claude Opus 4.6 --- docs/releases.md | 550 ++++++++++--------- src/main/storage/opencode-session-storage.ts | 34 +- 2 files changed, 310 insertions(+), 274 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index d053eb9477..81e0e7f113 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -17,60 +17,60 @@ Maestro can update itself automatically! This feature was introduced in **v0.8.7 **Latest: v0.15.2** | Released March 12, 2026 -# Major 0.15.x Additions - -🎶 **Maestro Symphony** — Contribute to open source with AI assistance! Browse curated issues from projects with the `runmaestro.ai` label, clone repos with one click, and automatically process the relevant Auto Run playbooks. Track your contributions, streaks, and stats. You're contributing CPU and tokens towards your favorite open source projects and features. - -🎬 **Director's Notes** — Aggregates history across all agents into a unified timeline with search, filters, and an activity graph. Includes an AI Overview tab that generates a structured synopsis of recent work. Off by default, gated behind a new "Encore Features" panel under settings. This is a precursor to an eventual plugin system, allowing for extensions and customizations without bloating the core app. - -🏷️ **Conductor Profile** — Available under Settings > General. Provide a short description on how Maestro agents should interface with you. - -🧠 **Three-State Thinking Toggle** — The thinking toggle now cycles through three modes: off, on, and sticky. Sticky mode keeps thinking content visible after the response completes. Cycle with CMD/CTRL+SHIFT+K. - -🤖 **Factory.ai Droid Support** — Added support for the [Factory.ai](https://factory.ai/product/cli) droid agent. Full session management and output parsing integration. - -## Change in v0.15.2 - -Patch release with bug fixes, UX improvements, and cherry-picks from the 0.16.0 RC. - -### New Features - -- **Cmd+0 → Last Tab:** Remapped Cmd+0 to jump to last tab; Cmd+Shift+0 now resets font size -- **Unsent draft protection:** Confirm dialog before closing tabs with unsent draft input -- **Read-only CLI flag:** Added `--read-only` flag to `maestro-cli send` command -- **Gemini read-only enforcement:** Gemini `-y` flag now works in read-only mode -- **Capability-based providers:** Replaced hardcoded agent ID checks with capability flags and shared metadata - -### Bug Fixes - -- **Sticky overlay scroll:** Fixed sticky overlays breaking tab scroll-into-view -- **Director's Notes stats:** Count only agents with entries in lookback window -- **SSH remote config:** Check `sessionSshRemoteConfig` as primary SSH remote ID source -- **.maestro file tree:** Always show .maestro directory even when dotfiles are hidden -- **Provider hardening:** Prototype safety, capability gates, stale map cleanup -- **Session search:** Per-session error resilience and metadata-based title matching -- **File tree stale loads:** Load sequence counter prevents stale file tree updates -- **File tree Unicode:** NFC normalization prevents duplicate entries -- **File tree duplicates:** Tree-structured data resolves duplicate entries -- **File tree auto-refresh:** Timer no longer destroyed on right panel tab switch -- **Menu z-index:** Branding header menu renders above sidebar content -- **Dropdown clipping:** Fixed hamburger menu and live overlay dropdown clipping -- **Font size shortcuts:** Restored Cmd+/- font size shortcuts lost with custom menu -- **Draft input preservation:** Replaying a previous message no longer discards current draft -- **SSH directory collision:** Skip warning when agents are on different SSH hosts -- **IPC error handling:** Handle expected IPC errors gracefully -- **Auto-focus on mode switch:** Input field auto-focuses when toggling AI/Shell mode -- **OpenCode parser:** Preserve JSON error events; reset resultEmitted on step_start -- **NDJSON performance:** Eliminated triple JSON parsing on hot path -- **Agent config overrides:** Apply config overrides in context groomer before spawning -- **Stale closure fix:** Resolved model not saving in wizard agent config - -### Visual Polish - -- **Light theme contrast:** Improved syntax highlighting contrast across all light themes -- **Context warning sash:** Dark text colors in light mode for readability -- **Session name dimming:** Use `textMain` color to prevent visual dimming -- **Session name pill:** Allow shrinking so date doesn't collide with type pill +# Major 0.15.x Additions + +🎶 **Maestro Symphony** — Contribute to open source with AI assistance! Browse curated issues from projects with the `runmaestro.ai` label, clone repos with one click, and automatically process the relevant Auto Run playbooks. Track your contributions, streaks, and stats. You're contributing CPU and tokens towards your favorite open source projects and features. + +🎬 **Director's Notes** — Aggregates history across all agents into a unified timeline with search, filters, and an activity graph. Includes an AI Overview tab that generates a structured synopsis of recent work. Off by default, gated behind a new "Encore Features" panel under settings. This is a precursor to an eventual plugin system, allowing for extensions and customizations without bloating the core app. + +🏷️ **Conductor Profile** — Available under Settings > General. Provide a short description on how Maestro agents should interface with you. + +🧠 **Three-State Thinking Toggle** — The thinking toggle now cycles through three modes: off, on, and sticky. Sticky mode keeps thinking content visible after the response completes. Cycle with CMD/CTRL+SHIFT+K. + +🤖 **Factory.ai Droid Support** — Added support for the [Factory.ai](https://factory.ai/product/cli) droid agent. Full session management and output parsing integration. + +## Change in v0.15.2 + +Patch release with bug fixes, UX improvements, and cherry-picks from the 0.16.0 RC. + +### New Features + +- **Cmd+0 → Last Tab:** Remapped Cmd+0 to jump to last tab; Cmd+Shift+0 now resets font size +- **Unsent draft protection:** Confirm dialog before closing tabs with unsent draft input +- **Read-only CLI flag:** Added `--read-only` flag to `maestro-cli send` command +- **Gemini read-only enforcement:** Gemini `-y` flag now works in read-only mode +- **Capability-based providers:** Replaced hardcoded agent ID checks with capability flags and shared metadata + +### Bug Fixes + +- **Sticky overlay scroll:** Fixed sticky overlays breaking tab scroll-into-view +- **Director's Notes stats:** Count only agents with entries in lookback window +- **SSH remote config:** Check `sessionSshRemoteConfig` as primary SSH remote ID source +- **.maestro file tree:** Always show .maestro directory even when dotfiles are hidden +- **Provider hardening:** Prototype safety, capability gates, stale map cleanup +- **Session search:** Per-session error resilience and metadata-based title matching +- **File tree stale loads:** Load sequence counter prevents stale file tree updates +- **File tree Unicode:** NFC normalization prevents duplicate entries +- **File tree duplicates:** Tree-structured data resolves duplicate entries +- **File tree auto-refresh:** Timer no longer destroyed on right panel tab switch +- **Menu z-index:** Branding header menu renders above sidebar content +- **Dropdown clipping:** Fixed hamburger menu and live overlay dropdown clipping +- **Font size shortcuts:** Restored Cmd+/- font size shortcuts lost with custom menu +- **Draft input preservation:** Replaying a previous message no longer discards current draft +- **SSH directory collision:** Skip warning when agents are on different SSH hosts +- **IPC error handling:** Handle expected IPC errors gracefully +- **Auto-focus on mode switch:** Input field auto-focuses when toggling AI/Shell mode +- **OpenCode parser:** Preserve JSON error events; reset resultEmitted on step_start +- **NDJSON performance:** Eliminated triple JSON parsing on hot path +- **Agent config overrides:** Apply config overrides in context groomer before spawning +- **Stale closure fix:** Resolved model not saving in wizard agent config + +### Visual Polish + +- **Light theme contrast:** Improved syntax highlighting contrast across all light themes +- **Context warning sash:** Dark text colors in light mode for readability +- **Session name dimming:** Use `textMain` color to prevent visual dimming +- **Session name pill:** Allow shrinking so date doesn't collide with type pill - **Scroll-to-bottom arrow:** Removed noisy indicator from terminal output view ### Previous Releases in this Series @@ -83,41 +83,41 @@ Patch release with bug fixes, UX improvements, and cherry-picks from the 0.16.0 **Latest: v0.14.5** | Released January 24, 2026 -Changes in this point release include: - -- Desktop app performance improvements (more to come on this, we want Maestro blazing fast) 🐌 -- Added local manifest feature for custom playbooks 📖 -- Agents are now inherently aware of your activity history as seen in the history panel 📜 (this is built-in cross context memory!) -- Added markdown rendering support for AI responses in mobile view 📱 -- Bugfix in tracking costs from JSONL files that were aged out 🏦 -- Added BlueSky social media handle for leaderboard 🦋 -- Added options to disable GPU rendering and confetti 🎊 -- Better handling of large files in preview 🗄️ -- Bug fix in Claude context calculation 🧮 -- Addressed bug in OpenSpec version reporting 🐛 - -The major contributions to 0.14.x remain: - -🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs. - -📶 SSH support for agents. Manage a remote agent with feature parity over SSH. Includes support for Git and File tree panels. Manage agents on remote systems or in containers. This even works for Group Chat, which is rad as hell. - -🧙‍♂️ Added an in-tab wizard for generating Auto Run Playbooks via `/wizard` or a new button in the Auto Run panel. - -# Smaller Changes in 014.x - -- Improved User Dashboard, available from hamburger menu, command palette or hotkey 🎛️ -- Leaderboard tracking now works across multiple systems and syncs level from cloud 🏆 -- Agent duplication. Pro tip: Consider a group of unused "Template" agents ✌️ -- New setting to prevent system from going to sleep while agents are active 🛏️ -- The tab menu has a new "Publish as GitHub Gist" option 📝 -- The tab menu has options to move the tab to the first or last position 🔀 -- [Maestro-Playbooks](https://github.com/pedramamini/Maestro-Playbooks) can now contain non-markdown assets 📙 -- Improved default shell detection 🐚 -- Added logic to prevent overlapping TTS notifications 💬 -- Added "Toggle Bookmark" shortcut (CTRL/CMD+SHIFT+B) ⌨️ -- Gist publishing now shows previous URLs with copy button 📋 - +Changes in this point release include: + +- Desktop app performance improvements (more to come on this, we want Maestro blazing fast) 🐌 +- Added local manifest feature for custom playbooks 📖 +- Agents are now inherently aware of your activity history as seen in the history panel 📜 (this is built-in cross context memory!) +- Added markdown rendering support for AI responses in mobile view 📱 +- Bugfix in tracking costs from JSONL files that were aged out 🏦 +- Added BlueSky social media handle for leaderboard 🦋 +- Added options to disable GPU rendering and confetti 🎊 +- Better handling of large files in preview 🗄️ +- Bug fix in Claude context calculation 🧮 +- Addressed bug in OpenSpec version reporting 🐛 + +The major contributions to 0.14.x remain: + +🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs. + +📶 SSH support for agents. Manage a remote agent with feature parity over SSH. Includes support for Git and File tree panels. Manage agents on remote systems or in containers. This even works for Group Chat, which is rad as hell. + +🧙‍♂️ Added an in-tab wizard for generating Auto Run Playbooks via `/wizard` or a new button in the Auto Run panel. + +# Smaller Changes in 014.x + +- Improved User Dashboard, available from hamburger menu, command palette or hotkey 🎛️ +- Leaderboard tracking now works across multiple systems and syncs level from cloud 🏆 +- Agent duplication. Pro tip: Consider a group of unused "Template" agents ✌️ +- New setting to prevent system from going to sleep while agents are active 🛏️ +- The tab menu has a new "Publish as GitHub Gist" option 📝 +- The tab menu has options to move the tab to the first or last position 🔀 +- [Maestro-Playbooks](https://github.com/pedramamini/Maestro-Playbooks) can now contain non-markdown assets 📙 +- Improved default shell detection 🐚 +- Added logic to prevent overlapping TTS notifications 💬 +- Added "Toggle Bookmark" shortcut (CTRL/CMD+SHIFT+B) ⌨️ +- Gist publishing now shows previous URLs with copy button 📋 + Thanks for the contributions: @t1mmen @aejfager @Crumbgrabber @whglaser @b3nw @deandebeer @shadown @breki @charles-dyfis-net @ronaldeddings @jlengrand @ksylvan ### Previous Releases in this Series @@ -136,20 +136,22 @@ Thanks for the contributions: @t1mmen @aejfager @Crumbgrabber @whglaser @b3nw @d ### Changes -- TAKE TWO! Fixed Linux ARM64 build architecture contamination issues 🏗️ - -### v0.13.1 Changes -- Fixed Linux ARM64 build architecture contamination issues 🏗️ -- Enhanced error handling for Auto Run batch processing 🚨 - -### v0.13.0 Changes -- Added a global usage dashboard, data collection begins with this install 🎛️ -- Added a Playbook Exchange for downloading pre-defined Auto Run playbooks from [Maestro-Playbooks](https://github.com/pedramamini/Maestro-Playbooks) 📕 -- Bundled OpenSpec commands for structured change proposals 📝 -- Added pre-release channel support for beta/RC updates 🧪 -- Implemented global hands-on time tracking across sessions ⏱️ -- Added new keyboard shortcut for agent settings (Opt+Cmd+, | Ctrl+Alt+,) ⌨️ -- Added directory size calculation with file/folder counts in file explorer 📊 +- TAKE TWO! Fixed Linux ARM64 build architecture contamination issues 🏗️ + +### v0.13.1 Changes + +- Fixed Linux ARM64 build architecture contamination issues 🏗️ +- Enhanced error handling for Auto Run batch processing 🚨 + +### v0.13.0 Changes + +- Added a global usage dashboard, data collection begins with this install 🎛️ +- Added a Playbook Exchange for downloading pre-defined Auto Run playbooks from [Maestro-Playbooks](https://github.com/pedramamini/Maestro-Playbooks) 📕 +- Bundled OpenSpec commands for structured change proposals 📝 +- Added pre-release channel support for beta/RC updates 🧪 +- Implemented global hands-on time tracking across sessions ⏱️ +- Added new keyboard shortcut for agent settings (Opt+Cmd+, | Ctrl+Alt+,) ⌨️ +- Added directory size calculation with file/folder counts in file explorer 📊 - Added sleep detection to exclude laptop sleep from time tracking ⏰ ### Previous Releases in this Series @@ -163,22 +165,26 @@ Thanks for the contributions: @t1mmen @aejfager @Crumbgrabber @whglaser @b3nw @d **Latest: v0.12.3** | Released December 28, 2025 -The big changes in the v0.12.x line are the following three: - -## Show Thinking -🤔 There is now a toggle to show thinking for the agent, the default for new tabs is off, though this can be changed under Settings > General. The toggle shows next to History and Read-Only. Very similar pattern. This has been the #1 most requested feature, though personally, I don't think I'll use it as I prefer to not see the details of the work, but the results of the work. Just as we work with our colleagues. - -## GitHub Spec-Kit Integration -🎯 Added [GitHub Spec-Kit](https://github.com/github/spec-kit) commands into Maestro with a built in updater to grab the latest prompts from the repository. We do override `/speckit-implement` (the final step) to create Auto Run docs and guide the user through their execution, which thanks to Wortrees from v0.11.x allows us to run in parallel! - -## Context Management Tools -📖 Added context management options from tab right-click menu. You can now compress, merge, and transfer contexts between agents. You will received (configurable) warnings at 60% and 80% context consumption with a hint to compact. - -## Changes Specific to v0.12.3: -- We now have hosted documentation through Mintlify 📚 -- Export any tab conversation as self-contained themed HTML file 📄 -- Publish files as private/public Gists 🌐 -- Added tab hover overlay menu with close operations and export 📋 +The big changes in the v0.12.x line are the following three: + +## Show Thinking + +🤔 There is now a toggle to show thinking for the agent, the default for new tabs is off, though this can be changed under Settings > General. The toggle shows next to History and Read-Only. Very similar pattern. This has been the #1 most requested feature, though personally, I don't think I'll use it as I prefer to not see the details of the work, but the results of the work. Just as we work with our colleagues. + +## GitHub Spec-Kit Integration + +🎯 Added [GitHub Spec-Kit](https://github.com/github/spec-kit) commands into Maestro with a built in updater to grab the latest prompts from the repository. We do override `/speckit-implement` (the final step) to create Auto Run docs and guide the user through their execution, which thanks to Wortrees from v0.11.x allows us to run in parallel! + +## Context Management Tools + +📖 Added context management options from tab right-click menu. You can now compress, merge, and transfer contexts between agents. You will received (configurable) warnings at 60% and 80% context consumption with a hint to compact. + +## Changes Specific to v0.12.3: + +- We now have hosted documentation through Mintlify 📚 +- Export any tab conversation as self-contained themed HTML file 📄 +- Publish files as private/public Gists 🌐 +- Added tab hover overlay menu with close operations and export 📋 - Added social handles to achievement share images 🏆 ### Previous Releases in this Series @@ -192,12 +198,12 @@ The big changes in the v0.12.x line are the following three: **Latest: v0.11.0** | Released December 22, 2025 -🌳 Github Worktree support was added. Any agent bound to a Git repository has the option to enable worktrees, each of which show up as a sub-agent with their own write-lock and Auto Run capability. Now you can truly develop in parallel on the same project and issue PRs when you're ready, all from within Maestro. Huge improvement, major thanks to @petersilberman. - -# Other Changes - -- @ file mentions now include documents from your Auto Run folder (which may not live in your agent working directory) 🗄️ -- The wizard is now capable of detecting and continuing on past started projects 🧙 +🌳 Github Worktree support was added. Any agent bound to a Git repository has the option to enable worktrees, each of which show up as a sub-agent with their own write-lock and Auto Run capability. Now you can truly develop in parallel on the same project and issue PRs when you're ready, all from within Maestro. Huge improvement, major thanks to @petersilberman. + +# Other Changes + +- @ file mentions now include documents from your Auto Run folder (which may not live in your agent working directory) 🗄️ +- The wizard is now capable of detecting and continuing on past started projects 🧙 - Bug fixes 🐛🐜🐞 --- @@ -208,14 +214,14 @@ The big changes in the v0.12.x line are the following three: ### Changes -- Export group chats as self-contained HTML ⬇️ -- Enhanced system process viewer now has details view with full process args 💻 -- Update button hides until platform binaries are available in releases. ⏳ -- Added Auto Run stall detection at the loop level, if no documents are updated after a loop 🔁 -- Improved Codex session discovery 🔍 -- Windows compatibility fixes 🐛 -- 64-bit Linux ARM build issue fixed (thanks @LilYoopug) 🐜 -- Addressed session enumeration issues with Codex and OpenCode 🐞 +- Export group chats as self-contained HTML ⬇️ +- Enhanced system process viewer now has details view with full process args 💻 +- Update button hides until platform binaries are available in releases. ⏳ +- Added Auto Run stall detection at the loop level, if no documents are updated after a loop 🔁 +- Improved Codex session discovery 🔍 +- Windows compatibility fixes 🐛 +- 64-bit Linux ARM build issue fixed (thanks @LilYoopug) 🐜 +- Addressed session enumeration issues with Codex and OpenCode 🐞 - Addressed pathing issues around gh command (thanks @oliveiraantoniocc) 🐝 ### Previous Releases in this Series @@ -231,13 +237,13 @@ The big changes in the v0.12.x line are the following three: ### Changes -- Add Sentry crashing reporting monitoring with opt-out 🐛 -- Stability fixes on v0.9.0 along with all the changes it brought along, including... - - Major refactor to enable supporting of multiple providers 👨‍👩‍👧‍👦 - - Added OpenAI Codex support 👨‍💻 - - Added OpenCode support 👩‍💻 - - Error handling system detects and recovers from agent failures 🚨 - - Added option to specify CLI arguments to AI providers ✨ +- Add Sentry crashing reporting monitoring with opt-out 🐛 +- Stability fixes on v0.9.0 along with all the changes it brought along, including... + - Major refactor to enable supporting of multiple providers 👨‍👩‍👧‍👦 + - Added OpenAI Codex support 👨‍💻 + - Added OpenCode support 👩‍💻 + - Error handling system detects and recovers from agent failures 🚨 + - Added option to specify CLI arguments to AI providers ✨ - Bunch of other little tweaks and additions 💎 ### Previous Releases in this Series @@ -252,19 +258,19 @@ The big changes in the v0.12.x line are the following three: ### Changes -- Added "Nudge" messages. Short static copy to include with every interactive message sent, perhaps to remind the agent on how to work 📌 -- Addressed various resource consumption issues to reduce battery cost 📉 -- Implemented fuzzy file search in quick actions for instant navigation 🔍 -- Added "clear" command support to clean terminal shell logs 🧹 -- Simplified search highlighting by integrating into markdown pipeline ✨ -- Enhanced update checker to filter prerelease tags like -rc, -beta 🚀 -- Fixed RPM package compatibility for OpenSUSE Tumbleweed 🐧 (H/T @JOduMonT) -- Added libuuid1 support alongside standard libuuid dependency 📦 -- Introduced Cmd+Shift+U shortcut for tab unread toggle ⌨️ -- Enhanced keyboard navigation for marking tabs unread 🎯 -- Expanded Linux distribution support with smart dependencies 🌐 -- Major underlying code re-structuring for maintainability 🧹 -- Improved stall detection to allow for individual docs to stall out while not affecting the entire playbook 📖 (H/T @mattjay) +- Added "Nudge" messages. Short static copy to include with every interactive message sent, perhaps to remind the agent on how to work 📌 +- Addressed various resource consumption issues to reduce battery cost 📉 +- Implemented fuzzy file search in quick actions for instant navigation 🔍 +- Added "clear" command support to clean terminal shell logs 🧹 +- Simplified search highlighting by integrating into markdown pipeline ✨ +- Enhanced update checker to filter prerelease tags like -rc, -beta 🚀 +- Fixed RPM package compatibility for OpenSUSE Tumbleweed 🐧 (H/T @JOduMonT) +- Added libuuid1 support alongside standard libuuid dependency 📦 +- Introduced Cmd+Shift+U shortcut for tab unread toggle ⌨️ +- Enhanced keyboard navigation for marking tabs unread 🎯 +- Expanded Linux distribution support with smart dependencies 🌐 +- Major underlying code re-structuring for maintainability 🧹 +- Improved stall detection to allow for individual docs to stall out while not affecting the entire playbook 📖 (H/T @mattjay) - Added option to select a static listening port for remote control 🎮 (H/T @b3nw) ### Previous Releases in this Series @@ -284,35 +290,40 @@ The big changes in the v0.12.x line are the following three: **Latest: v0.7.4** | Released December 12, 2025 -Minor bugfixes on top of v0.7.3: - -# Onboarding, Wizard, and Tours -- Implemented comprehensive onboarding wizard with integrated tour system 🚀 -- Added project-understanding confidence display to wizard UI 🎨 -- Enhanced keyboard navigation across all wizard screens ⌨️ -- Added analytics tracking for wizard and tour completion 📈 -- Added First Run Celebration modal with confetti animation 🎉 - -# UI / UX Enhancements -- Added expand-to-fullscreen button for Auto Run interface 🖥️ -- Created dedicated modal component and improved modal priority constants for expanded Auto Run view 📐 -- Enhanced user experience with fullscreen editing capabilities ✨ -- Fixed tab name display to correctly show full name for active tabs 🏷️ -- Added performance optimizations with throttling and caching for scrolling ⚡ -- Implemented drag-and-drop reordering for execution queue items 🎯 -- Enhanced toast context with agent name for OS notifications 📢 - -# Auto Run Workflow Improvements -- Created phase document generation for Auto Run workflow 📄 -- Added real-time log streaming to the LogViewer component 📊 - -# Application Behavior / Core Fixes -- Added validation to prevent nested worktrees inside the main repository 🚫 -- Fixed process manager to properly emit exit events on errors 🔧 -- Fixed process exit handling to ensure proper cleanup 🧹 - -# Update System -- Implemented automatic update checking on application startup 🚀 +Minor bugfixes on top of v0.7.3: + +# Onboarding, Wizard, and Tours + +- Implemented comprehensive onboarding wizard with integrated tour system 🚀 +- Added project-understanding confidence display to wizard UI 🎨 +- Enhanced keyboard navigation across all wizard screens ⌨️ +- Added analytics tracking for wizard and tour completion 📈 +- Added First Run Celebration modal with confetti animation 🎉 + +# UI / UX Enhancements + +- Added expand-to-fullscreen button for Auto Run interface 🖥️ +- Created dedicated modal component and improved modal priority constants for expanded Auto Run view 📐 +- Enhanced user experience with fullscreen editing capabilities ✨ +- Fixed tab name display to correctly show full name for active tabs 🏷️ +- Added performance optimizations with throttling and caching for scrolling ⚡ +- Implemented drag-and-drop reordering for execution queue items 🎯 +- Enhanced toast context with agent name for OS notifications 📢 + +# Auto Run Workflow Improvements + +- Created phase document generation for Auto Run workflow 📄 +- Added real-time log streaming to the LogViewer component 📊 + +# Application Behavior / Core Fixes + +- Added validation to prevent nested worktrees inside the main repository 🚫 +- Fixed process manager to properly emit exit events on errors 🔧 +- Fixed process exit handling to ensure proper cleanup 🧹 + +# Update System + +- Implemented automatic update checking on application startup 🚀 - Added settings toggle for enabling/disabling startup update checks ⚙️ ### Previous Releases in this Series @@ -328,38 +339,40 @@ Minor bugfixes on top of v0.7.3: **Latest: v0.6.1** | Released December 4, 2025 -In this release... -- Added recursive subfolder support for Auto Run markdown files 🗂️ -- Enhanced document tree display with expandable folder navigation 🌳 -- Enabled creating documents in subfolders with path selection 📁 -- Improved batch runner UI with inline progress bars and loop indicators 📊 -- Fixed execution queue display bug for immediate command processing 🐛 -- Added folder icons and better visual hierarchy for document browser 🎨 -- Implemented dynamic task re-counting for batch run loop iterations 🔄 -- Enhanced create document modal with location selector dropdown 📍 -- Improved progress tracking with per-document completion visualization 📈 -- Added support for nested folder structures in document management 🏗️ - -Plus the pre-release ALPHA... -- Template vars now set context in default autorun prompt 🚀 -- Added Enter key support for queued message confirmation dialog ⌨️ -- Kill process capability added to System Process Monitor 💀 -- Toggle markdown rendering added to Cmd+K Quick Actions 📝 -- Fixed cloudflared detection in packaged app environments 🔧 -- Added debugging logs for process exit diagnostics 🐛 -- Tab switcher shows last activity timestamps and filters by project 🕐 -- Slash commands now fill text on Tab/Enter instead of executing ⚡ -- Added GitHub Actions workflow for auto-assigning issues/PRs 🤖 -- Graceful handling for playbooks with missing documents implemented ✨ -- Added multi-document batch processing for Auto Run 🚀 -- Introduced Git worktree support for parallel execution 🌳 -- Created playbook system for saving run configurations 📚 -- Implemented document reset-on-completion with loop mode 🔄 -- Added drag-and-drop document reordering interface 🎯 -- Built Auto Run folder selector with file management 📁 -- Enhanced progress tracking with per-document metrics 📊 -- Integrated PR creation after worktree completion 🔀 -- Added undo/redo support in document editor ↩️ +In this release... + +- Added recursive subfolder support for Auto Run markdown files 🗂️ +- Enhanced document tree display with expandable folder navigation 🌳 +- Enabled creating documents in subfolders with path selection 📁 +- Improved batch runner UI with inline progress bars and loop indicators 📊 +- Fixed execution queue display bug for immediate command processing 🐛 +- Added folder icons and better visual hierarchy for document browser 🎨 +- Implemented dynamic task re-counting for batch run loop iterations 🔄 +- Enhanced create document modal with location selector dropdown 📍 +- Improved progress tracking with per-document completion visualization 📈 +- Added support for nested folder structures in document management 🏗️ + +Plus the pre-release ALPHA... + +- Template vars now set context in default autorun prompt 🚀 +- Added Enter key support for queued message confirmation dialog ⌨️ +- Kill process capability added to System Process Monitor 💀 +- Toggle markdown rendering added to Cmd+K Quick Actions 📝 +- Fixed cloudflared detection in packaged app environments 🔧 +- Added debugging logs for process exit diagnostics 🐛 +- Tab switcher shows last activity timestamps and filters by project 🕐 +- Slash commands now fill text on Tab/Enter instead of executing ⚡ +- Added GitHub Actions workflow for auto-assigning issues/PRs 🤖 +- Graceful handling for playbooks with missing documents implemented ✨ +- Added multi-document batch processing for Auto Run 🚀 +- Introduced Git worktree support for parallel execution 🌳 +- Created playbook system for saving run configurations 📚 +- Implemented document reset-on-completion with loop mode 🔄 +- Added drag-and-drop document reordering interface 🎯 +- Built Auto Run folder selector with file management 📁 +- Enhanced progress tracking with per-document metrics 📊 +- Integrated PR creation after worktree completion 🔀 +- Added undo/redo support in document editor ↩️ - Implemented auto-save with 5-second debounce 💾 ### Previous Releases in this Series @@ -374,15 +387,15 @@ Plus the pre-release ALPHA... ### Changes -- Added "Made with Maestro" badge to README header 🎯 -- Redesigned app icon with darker purple color scheme 🎨 -- Created new SVG badge for project attribution 🏷️ -- Added side-by-side image diff viewer for git changes 🖼️ -- Enhanced confetti animation with realistic cannon-style bursts 🎊 -- Fixed z-index layering for standing ovation overlay 📊 -- Improved tab switcher to show all named sessions 🔍 -- Enhanced batch synopsis prompts for cleaner summaries 📝 -- Added binary file detection in git diff parser 🔧 +- Added "Made with Maestro" badge to README header 🎯 +- Redesigned app icon with darker purple color scheme 🎨 +- Created new SVG badge for project attribution 🏷️ +- Added side-by-side image diff viewer for git changes 🖼️ +- Enhanced confetti animation with realistic cannon-style bursts 🎊 +- Fixed z-index layering for standing ovation overlay 📊 +- Improved tab switcher to show all named sessions 🔍 +- Enhanced batch synopsis prompts for cleaner summaries 📝 +- Added binary file detection in git diff parser 🔧 - Implemented git file reading at specific refs 📁 ### Previous Releases in this Series @@ -397,24 +410,24 @@ Plus the pre-release ALPHA... ### Changes -- Added Tab Switcher modal for quick navigation between AI tabs 🚀 -- Implemented @ mention file completion for AI mode references 📁 -- Added navigation history with back/forward through sessions and tabs ⏮️ -- Introduced tab completion filters for branches, tags, and files 🌳 -- Added unread tab indicators and filtering for better organization 📬 -- Implemented token counting display with human-readable formatting 🔢 -- Added markdown rendering toggle for AI responses in terminal 📝 -- Removed built-in slash commands in favor of custom AI commands 🎯 -- Added context menu for sessions with rename, bookmark, move options 🖱️ -- Enhanced file preview with stats showing size, tokens, timestamps 📊 -- Added token counting with js-tiktoken for file preview stats bar 🔢 -- Implemented Tab Switcher modal for fuzzy-search navigation (Opt+Cmd+T) 🔍 -- Added Save to History toggle (Cmd+S) for automatic work synopsis tracking 💾 -- Enhanced tab completion with @ mentions for file references in AI prompts 📎 -- Implemented navigation history with back/forward shortcuts (Cmd+Shift+,/.) 🔙 -- Added git branches and tags to intelligent tab completion system 🌿 -- Enhanced markdown rendering with syntax highlighting and toggle view 📝 -- Added right-click context menus for session management and organization 🖱️ +- Added Tab Switcher modal for quick navigation between AI tabs 🚀 +- Implemented @ mention file completion for AI mode references 📁 +- Added navigation history with back/forward through sessions and tabs ⏮️ +- Introduced tab completion filters for branches, tags, and files 🌳 +- Added unread tab indicators and filtering for better organization 📬 +- Implemented token counting display with human-readable formatting 🔢 +- Added markdown rendering toggle for AI responses in terminal 📝 +- Removed built-in slash commands in favor of custom AI commands 🎯 +- Added context menu for sessions with rename, bookmark, move options 🖱️ +- Enhanced file preview with stats showing size, tokens, timestamps 📊 +- Added token counting with js-tiktoken for file preview stats bar 🔢 +- Implemented Tab Switcher modal for fuzzy-search navigation (Opt+Cmd+T) 🔍 +- Added Save to History toggle (Cmd+S) for automatic work synopsis tracking 💾 +- Enhanced tab completion with @ mentions for file references in AI prompts 📎 +- Implemented navigation history with back/forward shortcuts (Cmd+Shift+,/.) 🔙 +- Added git branches and tags to intelligent tab completion system 🌿 +- Enhanced markdown rendering with syntax highlighting and toggle view 📝 +- Added right-click context menus for session management and organization 🖱️ - Improved mobile app with better WebSocket reconnection and status badges 📱 ### Previous Releases in this Series @@ -429,15 +442,15 @@ Plus the pre-release ALPHA... ### Changes -- Fixed tab handling requiring explicitly selected Claude session 🔧 -- Added auto-scroll navigation for slash command list selection ⚡ -- Implemented TTS audio feedback for toast notifications speak 🔊 -- Fixed shortcut case sensitivity using lowercase key matching 🔤 -- Added Cmd+Shift+J shortcut to jump to bottom instantly ⬇️ -- Sorted shortcuts alphabetically in help modal for discovery 📑 -- Display full commit message body in git log view 📝 -- Added expand/collapse all buttons to process tree header 🌳 -- Support synopsis process type in process tree parsing 🔍 +- Fixed tab handling requiring explicitly selected Claude session 🔧 +- Added auto-scroll navigation for slash command list selection ⚡ +- Implemented TTS audio feedback for toast notifications speak 🔊 +- Fixed shortcut case sensitivity using lowercase key matching 🔤 +- Added Cmd+Shift+J shortcut to jump to bottom instantly ⬇️ +- Sorted shortcuts alphabetically in help modal for discovery 📑 +- Display full commit message body in git log view 📝 +- Added expand/collapse all buttons to process tree header 🌳 +- Support synopsis process type in process tree parsing 🔍 - Renamed "No Group" to "UNGROUPED" for better clarity ✨ ### Previous Releases in this Series @@ -450,15 +463,15 @@ Plus the pre-release ALPHA... **Latest: v0.2.3** | Released November 29, 2025 -• Enhanced mobile web interface with session sync and history panel 📱 -• Added ThinkingStatusPill showing real-time token counts and elapsed time ⏱️ -• Implemented task count badges and session deduplication for batch runner 📊 -• Added TTS stop control and improved voice synthesis compatibility 🔊 -• Created image lightbox with navigation, clipboard, and delete features 🖼️ -• Fixed UI bugs in search, auto-scroll, and sidebar interactions 🐛 -• Added global Claude stats with streaming updates across projects 📈 -• Improved markdown checkbox styling and collapsed palette hover UX ✨ -• Enhanced scratchpad with search, image paste, and attachment support 🔍 +• Enhanced mobile web interface with session sync and history panel 📱 +• Added ThinkingStatusPill showing real-time token counts and elapsed time ⏱️ +• Implemented task count badges and session deduplication for batch runner 📊 +• Added TTS stop control and improved voice synthesis compatibility 🔊 +• Created image lightbox with navigation, clipboard, and delete features 🖼️ +• Fixed UI bugs in search, auto-scroll, and sidebar interactions 🐛 +• Added global Claude stats with streaming updates across projects 📈 +• Improved markdown checkbox styling and collapsed palette hover UX ✨ +• Enhanced scratchpad with search, image paste, and attachment support 🔍 • Added splash screen with logo and progress bar during startup 🎨 ### Previous Releases in this Series @@ -473,15 +486,15 @@ Plus the pre-release ALPHA... **Latest: v0.1.6** | Released November 27, 2025 -• Added template variables for dynamic AI command customization 🎯 -• Implemented session bookmarking with star icons and dedicated section ⭐ -• Enhanced Git Log Viewer with smarter date formatting 📅 -• Improved GitHub release workflow to handle partial failures gracefully 🔧 -• Added collapsible template documentation in AI Commands panel 📚 -• Updated default commit command with session ID traceability 🔍 -• Added tag indicators for custom-named sessions visually 🏷️ -• Improved Git Log search UX with better focus handling 🎨 -• Fixed input placeholder spacing for better readability 📝 +• Added template variables for dynamic AI command customization 🎯 +• Implemented session bookmarking with star icons and dedicated section ⭐ +• Enhanced Git Log Viewer with smarter date formatting 📅 +• Improved GitHub release workflow to handle partial failures gracefully 🔧 +• Added collapsible template documentation in AI Commands panel 📚 +• Updated default commit command with session ID traceability 🔍 +• Added tag indicators for custom-named sessions visually 🏷️ +• Improved Git Log search UX with better focus handling 🎨 +• Fixed input placeholder spacing for better readability 📝 • Updated documentation with new features and template references 📖 ### Previous Releases in this Series @@ -500,6 +513,7 @@ Plus the pre-release ALPHA... All releases are available on the [GitHub Releases page](https://github.com/RunMaestro/Maestro/releases). Maestro is available for: + - **macOS** - Apple Silicon (arm64) and Intel (x64) - **Windows** - x64 - **Linux** - x64 and arm64, AppImage, deb, and rpm packages diff --git a/src/main/storage/opencode-session-storage.ts b/src/main/storage/opencode-session-storage.ts index e8f8e1c555..7664011aab 100644 --- a/src/main/storage/opencode-session-storage.ts +++ b/src/main/storage/opencode-session-storage.ts @@ -225,7 +225,6 @@ function openOpenCodeDb(dbPath: string = OPENCODE_DB_PATH): Database.Database | return null; } const db = new Database(dbPath, { readonly: true, fileMustExist: true }); - db.pragma('journal_mode = WAL'); return db; } catch (error) { logger.warn(`Failed to open OpenCode SQLite database: ${error}`, LOG_CONTEXT); @@ -789,11 +788,12 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { // Also include global project sessions that match by directory field if (hasGlobalProject) { + const escapedPath = normalizedPath.replace(/[%_\\]/g, '\\$&'); const globalSessions = db .prepare( - "SELECT id, project_id, directory, title, version, time_created, time_updated, summary_additions, summary_deletions, summary_files FROM session WHERE project_id = 'global' AND (directory = ? OR directory LIKE ?) ORDER BY time_updated DESC" + "SELECT id, project_id, directory, title, version, time_created, time_updated, summary_additions, summary_deletions, summary_files FROM session WHERE project_id = 'global' AND (directory = ? OR directory LIKE ? ESCAPE '\\') ORDER BY time_updated DESC" ) - .all(normalizedPath, normalizedPath + '/%') as SqliteSessionRow[]; + .all(normalizedPath, escapedPath + '/%') as SqliteSessionRow[]; if (globalSessions.length > 0) { const existingIds = new Set(sessions.map((s) => s.id)); for (const gs of globalSessions) { @@ -902,6 +902,7 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { const partData = safeJsonParse(part.data); if (partData?.type === 'text' && partData.text?.trim()) { firstMessage = partData.text; + foundPreview = true; break; } } @@ -959,7 +960,18 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { ) .all(sessionId) as SqliteMessageRow[]; - if (messageRows.length === 0) return null; + // Session exists in SQLite but has no messages yet — return empty result + if (messageRows.length === 0) { + return { + messages: [], + parts: new Map(), + totalInputTokens: 0, + totalOutputTokens: 0, + totalCacheReadTokens: 0, + totalCacheWriteTokens: 0, + totalCost: 0, + }; + } const hasPartTable = tableExists(db, 'part'); const messages: OpenCodeMessage[] = []; @@ -1417,9 +1429,19 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { if (sshConfig) { return this.getRemoteMessageDir(sessionId); } - // For SQLite-backed sessions, return the database path + // Check if session exists in SQLite before returning DB path if (fsSync.existsSync(OPENCODE_DB_PATH)) { - return OPENCODE_DB_PATH; + const db = openOpenCodeDb(); + if (db) { + try { + const exists = tableExists(db, 'session') + ? db.prepare('SELECT 1 FROM session WHERE id = ? LIMIT 1').get(sessionId) + : null; + if (exists) return OPENCODE_DB_PATH; + } finally { + db.close(); + } + } } // Fallback to JSON message directory return this.getMessageDir(sessionId); From 1cd03ee395e9906fd74d54889355c95e7209ac3a Mon Sep 17 00:00:00 2001 From: chr1syy Date: Fri, 13 Mar 2026 22:14:17 +0100 Subject: [PATCH 3/5] fix: block deletion for all SQLite-backed sessions, not just non-empty ones Co-Authored-By: Claude Opus 4.6 --- src/main/storage/opencode-session-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/storage/opencode-session-storage.ts b/src/main/storage/opencode-session-storage.ts index 7664011aab..4db9bbe19c 100644 --- a/src/main/storage/opencode-session-storage.ts +++ b/src/main/storage/opencode-session-storage.ts @@ -1464,7 +1464,7 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { // Check if this session exists in SQLite — deletion not supported for SQLite sessions // (we open the DB read-only and shouldn't modify OpenCode's database) const sqliteResult = this.loadSessionMessagesSqlite(sessionId); - if (sqliteResult && sqliteResult.messages.length > 0) { + if (sqliteResult) { logger.warn( 'Delete message pair not supported for SQLite-backed OpenCode sessions', LOG_CONTEXT From 5999a664d1eb47fa26ec954b04568bdc041df0da Mon Sep 17 00:00:00 2001 From: chr1syy Date: Fri, 13 Mar 2026 22:41:59 +0100 Subject: [PATCH 4/5] fix: verify session exists in SQLite before blocking JSON fallback When no messages are found for a sessionId, check the session table to distinguish "session exists but empty" from "session not in SQLite" before deciding whether to block JSON fallback. Co-Authored-By: Claude Opus 4.6 --- src/main/storage/opencode-session-storage.ts | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/storage/opencode-session-storage.ts b/src/main/storage/opencode-session-storage.ts index 4db9bbe19c..b6eda3ec96 100644 --- a/src/main/storage/opencode-session-storage.ts +++ b/src/main/storage/opencode-session-storage.ts @@ -960,17 +960,25 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { ) .all(sessionId) as SqliteMessageRow[]; - // Session exists in SQLite but has no messages yet — return empty result if (messageRows.length === 0) { - return { - messages: [], - parts: new Map(), - totalInputTokens: 0, - totalOutputTokens: 0, - totalCacheReadTokens: 0, - totalCacheWriteTokens: 0, - totalCost: 0, - }; + // Verify session actually exists in SQLite before blocking JSON fallback + if (tableExists(db, 'session')) { + const sessionExists = db + .prepare('SELECT 1 FROM session WHERE id = ? LIMIT 1') + .get(sessionId); + if (sessionExists) { + return { + messages: [], + parts: new Map(), + totalInputTokens: 0, + totalOutputTokens: 0, + totalCacheReadTokens: 0, + totalCacheWriteTokens: 0, + totalCost: 0, + }; + } + } + return null; } const hasPartTable = tableExists(db, 'part'); From 2aed882f33874cd13ce92c95a4d3d8f14a1e6672 Mon Sep 17 00:00:00 2001 From: chr1syy Date: Fri, 13 Mar 2026 22:58:49 +0100 Subject: [PATCH 5/5] fix: address CodeRabbit review findings for OpenCode session storage - Use platform-aware path separators (path.sep) instead of hardcoded '/' for trailing separator stripping and subdirectory prefix comparisons, fixing Windows path matching in listSessions and findProjectId - Extract TRAILING_SEP_RE constant to avoid repeated regex construction - Improve openOpenCodeDb error handling: report unexpected SQLite errors (corruption, permissions) to Sentry and rethrow instead of silently falling back to JSON; file-not-found still returns null - Re-sort sessions after merging global project sessions so combined results maintain newest-first ordering by time_updated Co-Authored-By: Claude Opus 4.6 --- src/main/storage/opencode-session-storage.ts | 39 ++++++++++++-------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/storage/opencode-session-storage.ts b/src/main/storage/opencode-session-storage.ts index b6eda3ec96..06c48ec525 100644 --- a/src/main/storage/opencode-session-storage.ts +++ b/src/main/storage/opencode-session-storage.ts @@ -38,6 +38,9 @@ import { isWindows } from '../../shared/platformDetection'; const LOG_CONTEXT = '[OpenCodeSessionStorage]'; +/** Regex matching one or more trailing path separators (platform-aware) */ +const TRAILING_SEP_RE = new RegExp(`${path.sep.replace('\\', '\\\\')}+$`); + /** * Get OpenCode data base directory (platform-specific) * - Linux/macOS: ~/.local/share/opencode @@ -220,15 +223,18 @@ interface SqlitePartData { * Returns null if the database file doesn't exist. */ function openOpenCodeDb(dbPath: string = OPENCODE_DB_PATH): Database.Database | null { + if (!fsSync.existsSync(dbPath)) { + return null; + } try { - if (!fsSync.existsSync(dbPath)) { - return null; - } const db = new Database(dbPath, { readonly: true, fileMustExist: true }); return db; } catch (error) { - logger.warn(`Failed to open OpenCode SQLite database: ${error}`, LOG_CONTEXT); - return null; + logger.warn(`${LOG_CONTEXT} Failed to open OpenCode SQLite database at ${dbPath}: ${error}`); + captureException(error instanceof Error ? error : new Error(String(error)), { + extra: { dbPath }, + }); + throw error; } } @@ -395,8 +401,8 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { const projectFiles = await listJsonFiles(projectDir); - // Normalize project path for comparison (resolve and remove trailing slashes) - const normalizedPath = path.resolve(projectPath).replace(/\/+$/, ''); + // Normalize project path for comparison (resolve and remove trailing separators) + const normalizedPath = path.resolve(projectPath).replace(TRAILING_SEP_RE, ''); logger.info(`Looking for OpenCode project for path: ${normalizedPath}`, LOG_CONTEXT); for (const file of projectFiles) { @@ -407,7 +413,7 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { if (!projectData?.worktree) continue; // Normalize stored path the same way - const storedPath = path.resolve(projectData.worktree).replace(/\/+$/, ''); + const storedPath = path.resolve(projectData.worktree).replace(TRAILING_SEP_RE, ''); // Exact match if (storedPath === normalizedPath) { @@ -420,8 +426,8 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { // Check if one is a subdirectory of the other (handles worktree subdirs) if ( - normalizedPath.startsWith(storedPath + '/') || - storedPath.startsWith(normalizedPath + '/') + normalizedPath.startsWith(storedPath + path.sep) || + storedPath.startsWith(normalizedPath + path.sep) ) { logger.info( `Found OpenCode project (subdirectory match): ${projectData.id} for path: ${normalizedPath}`, @@ -748,7 +754,7 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { return null; } - const normalizedPath = path.resolve(projectPath).replace(/\/+$/, ''); + const normalizedPath = path.resolve(projectPath).replace(TRAILING_SEP_RE, ''); // Find matching project(s) — exact match or subdirectory match const projects = db.prepare('SELECT id, worktree FROM project').all() as Array<{ @@ -765,11 +771,11 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { hasGlobalProject = true; continue; } - const storedPath = path.resolve(proj.worktree).replace(/\/+$/, ''); + const storedPath = path.resolve(proj.worktree).replace(TRAILING_SEP_RE, ''); if ( storedPath === normalizedPath || - normalizedPath.startsWith(storedPath + '/') || - storedPath.startsWith(normalizedPath + '/') + normalizedPath.startsWith(storedPath + path.sep) || + storedPath.startsWith(normalizedPath + path.sep) ) { matchingProjectIds.push(proj.id); } @@ -793,7 +799,7 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { .prepare( "SELECT id, project_id, directory, title, version, time_created, time_updated, summary_additions, summary_deletions, summary_files FROM session WHERE project_id = 'global' AND (directory = ? OR directory LIKE ? ESCAPE '\\') ORDER BY time_updated DESC" ) - .all(normalizedPath, escapedPath + '/%') as SqliteSessionRow[]; + .all(normalizedPath, escapedPath + path.sep + '%') as SqliteSessionRow[]; if (globalSessions.length > 0) { const existingIds = new Set(sessions.map((s) => s.id)); for (const gs of globalSessions) { @@ -804,6 +810,9 @@ export class OpenCodeSessionStorage extends BaseSessionStorage { } } + // Re-sort after merging global sessions so newest come first + sessions.sort((a, b) => b.time_updated - a.time_updated); + if (sessions.length === 0) { logger.info(`No OpenCode sessions found in SQLite for: ${normalizedPath}`, LOG_CONTEXT); return [];