From 0a6c87c9ab881e3c521072f27b171a3a6a6792e7 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Mon, 22 Dec 2025 00:59:40 -0600 Subject: [PATCH] ## CHANGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added Git Worktrees for truly parallel, conflict-free multi-agent development! 🌳 - Introduced Worktree sub-agents nested under parents for clean organization! 🌿 - Built WorktreeConfigModal to manage base paths and watching behavior! ⚙️ - Added one-click Pull Request creation for worktree child sessions! 🔀 - Integrated Create PR actions into sidebar context menus and shortcuts! ⌨️ - Updated git status dropdown with “Configure Worktrees” and PR actions! 🧩 - Reworked Auto Run guidance to promote worktree sessions for parallelism! 🚀 - Removed Auto Run’s embedded worktree UI, simplifying batch runner flow! 🧹 - Simplified playbooks by dropping stored worktree settings entirely! 📓 - Polished tab starring safety by blocking stars on unsaved tabs! ⭐️ --- README.md | 78 +++-- src/renderer/App.tsx | 74 ++++- src/renderer/components/AutoRun.tsx | 4 +- .../components/AutoRunExpandedModal.tsx | 4 +- .../components/AutoRunnerHelpModal.tsx | 37 +-- src/renderer/components/BatchRunnerModal.tsx | 136 +------- src/renderer/components/MainPanel.tsx | 51 ++- src/renderer/components/QuickActionsModal.tsx | 14 +- src/renderer/components/SessionItem.tsx | 71 +++-- src/renderer/components/SessionList.tsx | 293 +++++++++++------- src/renderer/components/TabBar.tsx | 2 +- .../components/ThinkingStatusPill.tsx | 7 +- src/renderer/constants/modalPriorities.ts | 6 + src/renderer/global.d.ts | 21 ++ src/renderer/hooks/usePlaybookManagement.ts | 62 +--- src/renderer/types/index.ts | 13 +- 16 files changed, 478 insertions(+), 395 deletions(-) diff --git a/README.md b/README.md index 28dc8242..f8678d99 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Download the latest release for your platform from the [Releases](https://github ### Power Features +- 🌳 **[Git Worktrees](#git-worktrees)** - Run AI agents in parallel on isolated branches. Create worktree sub-agents from the git branch menu, each operating in their own directory. Work interactively in the main repo while sub-agents process tasks independently—then create PRs with one click. True parallel development without conflicts. - 🤖 **[Auto Run & Playbooks](#auto-run)** - File-system-based task runner that batch-processes markdown checklists through AI agents. Create playbooks for repeatable workflows, run in loops, and track progress with full history. Each task gets its own AI session for clean conversation context. - 💬 **[Group Chat](#group-chat)** - Coordinate multiple AI agents in a single conversation. A moderator AI orchestrates discussions, routing questions to the right agents and synthesizing their responses for cross-project questions and architecture discussions. - 🌐 **[Mobile Remote Control](#remote-access)** - Built-in web server with QR code access. Monitor and control all your agents from your phone. Supports local network access and remote tunneling via Cloudflare for access from anywhere. @@ -111,6 +112,7 @@ This approach mirrors methodologies like [Spec-Kit](https://github.com/github/sp | **Agent** | A workspace tied to a project directory and AI provider (Claude Code, Codex, or OpenCode). Contains one Command Terminal and one AI Terminal with full conversation history. | | **Group** | Organizational container for agents. Group by project, client, or workflow. | | **Group Chat** | Multi-agent conversation coordinated by a moderator. Ask questions across multiple agents and get synthesized answers. | +| **Git Worktree** | An isolated working directory linked to a separate branch. Worktree sub-agents appear nested under their parent in the session list and can create PRs. | | **AI Terminal** | The conversation interface with your AI agent. Supports `@` file mentions, slash commands, and image attachments. | | **Command Terminal** | A PTY shell session for running commands directly. Tab completion for files, git branches, and command history. | | **Session Explorer** | Browse all past conversations for an agent. Star, rename, search, and resume any previous session. | @@ -352,6 +354,57 @@ It's {{WEEKDAY}}, {{DATE}}. I'm on branch {{GIT_BRANCH}} at {{AGENT_PATH}}. Summarize what I worked on yesterday and suggest priorities for today. ``` +## Git Worktrees + +Git worktrees enable true parallel development by letting you run multiple AI agents on separate branches simultaneously. Each worktree operates in its own isolated directory, so there's no risk of conflicts between parallel work streams. + +### Creating a Worktree Sub-Agent + +1. In the session list, hover over an agent in a git repository +2. Click the **git branch indicator** (shows current branch name) +3. In the overlay menu, click **"Create Worktree Sub-Agent"** +4. Configure the worktree: + - **Worktree Directory** — Base folder where worktrees are created + - **Branch Name** — Name for the new branch (becomes the subdirectory name) + - **Create PR on Completion** — Auto-open a pull request when done + - **Target Branch** — Base branch for the PR (defaults to main/master) + +### How Worktree Sessions Work + +- **Nested Display** — Worktree sub-agents appear indented under their parent session in the left sidebar +- **Branch Icon** — A git branch icon indicates worktree sessions +- **Collapse/Expand** — Click the chevron on a parent session to show/hide its worktree children +- **Independent Operation** — Each worktree session has its own working directory, conversation history, and state + +### Creating Pull Requests + +When you're done with work in a worktree: + +1. **Right-click** the worktree session → **Create Pull Request**, or +2. Press **Cmd+K** with the worktree active → search "Create Pull Request" + +The PR modal shows: +- Source branch (your worktree branch) +- Target branch (configurable) +- Auto-generated title and description based on your work + +**Requirements:** GitHub CLI (`gh`) must be installed and authenticated. Maestro will detect if it's missing and show installation instructions. + +### Use Cases + +| Scenario | How Worktrees Help | +|----------|-------------------| +| **Background Auto Run** | Run Auto Run in a worktree while working interactively in the main repo | +| **Feature Branches** | Spin up a sub-agent for each feature branch | +| **Code Review** | Create a worktree to review and iterate on a PR without switching branches | +| **Parallel Experiments** | Try different approaches simultaneously without git stash/pop | + +### Tips + +- **Name branches descriptively** — The branch name becomes the worktree directory name +- **Use a dedicated worktree folder** — Keep all worktrees in one place (e.g., `~/worktrees/`) +- **Clean up when done** — Delete worktree sessions after merging PRs to avoid clutter + ## Auto Run Auto Run is a file-system-based document runner that lets you batch-process tasks using AI agents. Select a folder containing markdown documents with task checkboxes, and Maestro will work through them one by one, spawning a fresh AI session for each task. @@ -404,25 +457,6 @@ Save your batch configurations for reuse: 3. Load saved playbooks from the **Load Playbook** dropdown 4. Update or discard changes to loaded playbooks -### Git Worktree Support - -Git worktrees allow Auto Run to operate in an isolated directory while you continue working interactively in the main repository—no read-only restrictions, no waiting. - -**Setup:** -1. Enable **Worktree** in the Batch Runner Modal -2. Select a **Worktree Directory** (base folder for all worktrees) -3. Enter a **Branch Name** (becomes the worktree subdirectory) -4. The computed path shows where the worktree will be created -5. Optionally enable **Create PR on completion** to auto-open a pull request - -**Benefits:** -- **Continue working** in the main repo while Auto Run processes tasks in the background -- **No read-only mode** — the yellow border and input lock only appear when running without a worktree -- **Visual indicator** — a git branch icon appears in the AUTO badge, right panel, and status pill when using a worktree -- **Automatic PR creation** — when enabled, opens a PR from the worktree branch to your target branch - -**Without a worktree:** Auto Run queues with other write operations to prevent conflicts, and the editor enters read-only mode until completion. - ### Progress Tracking The runner will: @@ -490,9 +524,11 @@ Click the **Stop** button at any time. The runner will: - Preserve all completed work - Allow you to resume later by clicking Run again -### Parallel Batches +### Parallel Auto Runs -You can run separate batch processes in different Maestro sessions simultaneously. Each session maintains its own independent batch state. With Git worktrees enabled, you can work on the main branch while Auto Run operates in an isolated worktree. +Auto Run can execute in parallel across different agents without conflicts—each agent works in its own project directory, so there's no risk of clobbering each other's work. + +**Same project, parallel work:** To run multiple Auto Runs in the same repository simultaneously, create worktree sub-agents from the git branch menu (see [Git Worktrees](#git-worktrees)). Each worktree operates in an isolated directory with its own branch, enabling true parallel task execution on the same codebase. ## Group Chat diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 0244b4a6..22c72877 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -37,6 +37,8 @@ import { TourOverlay } from './components/Wizard/tour'; import { CONDUCTOR_BADGES, getBadgeForTime } from './constants/conductorBadges'; import { EmptyStateView } from './components/EmptyStateView'; import { AgentErrorModal } from './components/AgentErrorModal'; +import { WorktreeConfigModal } from './components/WorktreeConfigModal'; +import { CreatePRModal } from './components/CreatePRModal'; // Group Chat Components import { GroupChatPanel } from './components/GroupChatPanel'; @@ -390,6 +392,11 @@ export default function MaestroConsole() { // Agent Error Modal State - tracks which session has an active error being shown const [agentErrorModalSessionId, setAgentErrorModalSessionId] = useState(null); + // Worktree Modal State + const [worktreeConfigModalOpen, setWorktreeConfigModalOpen] = useState(false); + const [createPRModalOpen, setCreatePRModalOpen] = useState(false); + const [createPRSession, setCreatePRSession] = useState(null); + // Tab Switcher Modal State const [tabSwitcherOpen, setTabSwitcherOpen] = useState(false); @@ -3868,7 +3875,7 @@ export default function MaestroConsole() { // Create a group using the user-provided name const groupId = generateId(); - const worktreeGroup: SessionGroup = { + const worktreeGroup: Group = { id: groupId, name: name, collapsed: false, @@ -6208,6 +6215,10 @@ export default function MaestroConsole() { onDeleteGroupChat={deleteGroupChatWithConfirmation} activeGroupChatId={activeGroupChatId} hasActiveSessionCapability={hasActiveSessionCapability} + onOpenCreatePR={(session) => { + setCreatePRSession(session); + setCreatePRModalOpen(true); + }} onToggleRemoteControl={async () => { await toggleGlobalLive(); // Show flash notification based on the NEW state (opposite of current) @@ -6351,6 +6362,58 @@ export default function MaestroConsole() { /> )} + {/* --- WORKTREE CONFIG MODAL --- */} + {worktreeConfigModalOpen && activeSession && ( + setWorktreeConfigModalOpen(false)} + theme={theme} + session={activeSession} + worktreeChildren={sessions.filter(s => s.parentSessionId === activeSession.id)} + onSaveConfig={(config) => { + setSessions(prev => prev.map(s => + s.id === activeSession.id + ? { ...s, worktreeConfig: config } + : s + )); + }} + onCreateWorktree={async (branchName) => { + // TODO: Implement worktree creation via git worktree add + console.log('[WorktreeConfig] Create worktree:', branchName); + }} + onRemoveWorktree={(sessionId) => { + // Remove the worktree session + setSessions(prev => prev.filter(s => s.id !== sessionId)); + }} + onSelectWorktree={(sessionId) => { + setActiveSessionId(sessionId); + }} + /> + )} + + {/* --- CREATE PR MODAL --- */} + {createPRModalOpen && (createPRSession || activeSession) && ( + { + setCreatePRModalOpen(false); + setCreatePRSession(null); + }} + theme={theme} + worktreePath={(createPRSession || activeSession)!.cwd} + worktreeBranch={(createPRSession || activeSession)!.worktreeBranch || (createPRSession || activeSession)!.gitBranches?.[0] || 'main'} + availableBranches={(createPRSession || activeSession)!.gitBranches || ['main', 'master']} + onPRCreated={(prUrl) => { + addToast({ + type: 'success', + title: 'Pull Request Created', + message: prUrl, + }); + setCreatePRSession(null); + }} + /> + )} + {/* --- FIRST RUN CELEBRATION OVERLAY --- */} {firstRunCelebrationData && ( { if (!activeSession) return; + // Find the tab first to check if it has a session ID + const tabToStar = activeSession.aiTabs.find(t => t.id === tabId); + // Don't allow starring tabs without a session ID (new/empty tabs) + if (!tabToStar?.agentSessionId) return; setSessions(prev => prev.map(s => { if (s.id !== activeSession.id) return s; // Find the tab to get its agentSessionId for persistence @@ -7292,6 +7359,9 @@ export default function MaestroConsole() { setTimeout(() => setSuccessFlashNotification(null), 2000); }} onOpenFuzzySearch={() => setFuzzyFileSearchOpen(true)} + onOpenWorktreeConfig={() => setWorktreeConfigModalOpen(true)} + onOpenCreatePR={() => setCreatePRModalOpen(true)} + isWorktreeChild={!!activeSession?.parentSessionId} /> )} @@ -7393,8 +7463,6 @@ export default function MaestroConsole() { getDocumentTaskCount={getDocumentTaskCount} onRefreshDocuments={handleAutoRunRefresh} sessionId={activeSession.id} - sessionCwd={activeSession.cwd} - ghPath={ghPath} /> )} diff --git a/src/renderer/components/AutoRun.tsx b/src/renderer/components/AutoRun.tsx index 117fbda7..9175f3e8 100644 --- a/src/renderer/components/AutoRun.tsx +++ b/src/renderer/components/AutoRun.tsx @@ -1209,7 +1209,8 @@ const AutoRunInner = forwardRef(function AutoRunInn )} - {/* Image upload button - always visible, ghosted in preview mode */} + {/* Image upload button - hidden for now, can be re-enabled by removing false && */} + {false && ( + )} - {/* Image upload button - always visible, ghosted in preview mode */} + {/* Image upload button - hidden for now, can be re-enabled by removing false && */} + {false && ( + )}

The input shows a READ-ONLY indicator - as a reminder. This prevents conflicts between manual and automated work... + as a reminder. This prevents conflicts between manual and automated work.

- Unless you enable Git Worktree: -

- - - - {/* Git Worktree */} -
-
- -

Git Worktree (Parallel Work)

-
-
-

- For Git repositories, enable Git Worktree to - run Auto Run in an isolated working directory. This allows you to{' '} - continue making changes interactively{' '} - in the main repository while Auto Run processes tasks in the background—no read-only restrictions, - no yellow border, no waiting. -

-

- Select a worktree directory and branch name. The worktree will be created as a subdirectory - using the branch name. Optionally enable "Create PR on completion" to - automatically open a pull request when all tasks finish. -

-

- When running in a worktree, a - icon appears in the AUTO badge, right panel, and status pill to indicate parallel operation. + Tip: For parallel work without read-only restrictions, + create a worktree session from the git branch menu in the session list. Worktree sessions operate + in isolated directories, allowing Auto Run and manual work to happen simultaneously.

diff --git a/src/renderer/components/BatchRunnerModal.tsx b/src/renderer/components/BatchRunnerModal.tsx index e487df9c..6e443f21 100644 --- a/src/renderer/components/BatchRunnerModal.tsx +++ b/src/renderer/components/BatchRunnerModal.tsx @@ -8,8 +8,7 @@ import { PlaybookDeleteConfirmModal } from './PlaybookDeleteConfirmModal'; import { PlaybookNameModal } from './PlaybookNameModal'; import { AgentPromptComposerModal } from './AgentPromptComposerModal'; import { DocumentsPanel } from './DocumentsPanel'; -import { GitWorktreeSection, GhCliStatus } from './GitWorktreeSection'; -import { usePlaybookManagement, useWorktreeValidation } from '../hooks'; +import { usePlaybookManagement } from '../hooks'; import { autorunDefaultPrompt } from '../../prompts'; import { generateId } from '../utils/ids'; @@ -33,10 +32,6 @@ interface BatchRunnerModalProps { onRefreshDocuments: () => Promise; // Refresh document list from folder // Session ID for playbook storage sessionId: string; - // Session cwd for git worktree support - sessionCwd: string; - // Custom path to gh CLI binary (optional, for worktree features) - ghPath?: string; } // Helper function to count unchecked tasks in scratchpad content @@ -80,8 +75,6 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { getDocumentTaskCount, onRefreshDocuments, sessionId, - sessionCwd, - ghPath } = props; // Document list state @@ -113,45 +106,17 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { const [promptComposerOpen, setPromptComposerOpen] = useState(false); const textareaRef = useRef(null); - // Git worktree state - only show worktree section for git repos - const [isGitRepo, setIsGitRepo] = useState(false); - const [checkingGitRepo, setCheckingGitRepo] = useState(true); - - // Worktree configuration state - const [worktreeEnabled, setWorktreeEnabled] = useState(false); - const [worktreeBaseDir, setWorktreeBaseDir] = useState(''); // User-selected base directory for all worktrees - const [branchName, setBranchName] = useState(''); - const [createPROnCompletion, setCreatePROnCompletion] = useState(false); - const [prTargetBranch, setPrTargetBranch] = useState('main'); - const [availableBranches, setAvailableBranches] = useState([]); - const [ghCliStatus, setGhCliStatus] = useState(null); - - // Compute the display path for the UI (baseDir + branchName) - const computedWorktreePath = worktreeBaseDir && branchName - ? `${worktreeBaseDir.replace(/\/+$/, '')}/${branchName.replace(/[^a-zA-Z0-9-_]/g, '-')}` - : ''; - // Playbook management callback to apply loaded playbook configuration const handleApplyPlaybook = useCallback((data: { documents: BatchDocumentEntry[]; loopEnabled: boolean; maxLoops: number | null; prompt: string; - worktreeEnabled: boolean; - branchName: string; - createPROnCompletion: boolean; - prTargetBranch: string; }) => { setDocuments(data.documents); setLoopEnabled(data.loopEnabled); setMaxLoops(data.maxLoops); setPrompt(data.prompt); - setWorktreeEnabled(data.worktreeEnabled); - setBranchName(data.branchName); - setCreatePROnCompletion(data.createPROnCompletion); - if (data.prTargetBranch) { - setPrTargetBranch(data.prTargetBranch); - } }, []); // Playbook management hook @@ -186,22 +151,10 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { loopEnabled, maxLoops, prompt, - worktreeEnabled, - branchName, - createPROnCompletion, - prTargetBranch, }, onApplyPlaybook: handleApplyPlaybook, }); - // Worktree validation hook (debounced validation of worktree path) - const { validation: worktreeValidation } = useWorktreeValidation({ - worktreePath: computedWorktreePath, - branchName, - worktreeEnabled, - sessionCwd, - }); - const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack(); const layerIdRef = useRef(); @@ -230,50 +183,6 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { loadTaskCounts(); }, [allDocuments]); - // Check if session cwd is a git repo on mount (for worktree support) - useEffect(() => { - const checkGitRepo = async () => { - setCheckingGitRepo(true); - console.log(`[BatchRunnerModal] Checking git repo and gh CLI. ghPath prop: "${ghPath}"`); - try { - const result = await window.maestro.git.isRepo(sessionCwd); - const isRepo = result === true; - setIsGitRepo(isRepo); - - // If it's a git repo, fetch available branches and check gh CLI - if (isRepo) { - const ghPathToUse = ghPath || undefined; - console.log(`[BatchRunnerModal] Checking gh CLI with path: ${ghPathToUse}`); - const [branchResult, ghResult] = await Promise.all([ - window.maestro.git.branches(sessionCwd), - window.maestro.git.checkGhCli(ghPathToUse) - ]); - console.log(`[BatchRunnerModal] gh CLI check result:`, ghResult); - - if (branchResult.branches && branchResult.branches.length > 0) { - setAvailableBranches(branchResult.branches); - // Set default target branch to 'main' or 'master' if available - if (branchResult.branches.includes('main')) { - setPrTargetBranch('main'); - } else if (branchResult.branches.includes('master')) { - setPrTargetBranch('master'); - } else { - setPrTargetBranch(branchResult.branches[0]); - } - } - - setGhCliStatus(ghResult); - } - } catch (error) { - console.error('Failed to check if git repo:', error); - setIsGitRepo(false); - } - setCheckingGitRepo(false); - }; - - checkGitRepo(); - }, [sessionCwd, ghPath]); - // Calculate total tasks across selected documents (excluding missing documents) const totalTaskCount = documents.reduce((sum, doc) => { // Don't count tasks from missing documents @@ -354,7 +263,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { // Filter out missing documents before starting batch run const validDocuments = documents.filter(doc => !doc.isMissing); - // Build config with optional worktree settings + // Build config (worktree configuration is now managed separately via WorktreeConfigModal) const config: BatchRunConfig = { documents: validDocuments, prompt, @@ -362,29 +271,9 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { maxLoops: loopEnabled ? maxLoops : null }; - // Add worktree config if enabled and valid - if (worktreeEnabled && isGitRepo && worktreeBaseDir && branchName) { - // Compute the worktree path: baseDir + sanitized branch name - // e.g., /Users/pedram/worktrees + branch "feature-x" -> /Users/pedram/worktrees/feature-x - const sanitizedBranch = branchName.replace(/[^a-zA-Z0-9-_]/g, '-'); - const computedWorktreePath = `${worktreeBaseDir.replace(/\/+$/, '')}/${sanitizedBranch}`; - - config.worktree = { - enabled: true, - path: computedWorktreePath, - branchName, - createPROnCompletion, - prTargetBranch, - ghPath: ghPath || undefined - }; - } - console.log('[BatchRunnerModal] handleGo - calling onGo with config:', config); window.maestro.logger.log('info', 'Go button clicked', 'BatchRunnerModal', { documentsCount: validDocuments.length, - worktreeEnabled: config.worktree?.enabled, - worktreePath: config.worktree?.path, - branchName: config.worktree?.branchName }); onGo(config); @@ -599,27 +488,6 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { onRefreshDocuments={onRefreshDocuments} /> - {/* Git Worktree Section - only visible for git repos */} - {isGitRepo && !checkingGitRepo && ( - - )} - {/* Divider */}
diff --git a/src/renderer/components/MainPanel.tsx b/src/renderer/components/MainPanel.tsx index b04a93ed..a67cb378 100644 --- a/src/renderer/components/MainPanel.tsx +++ b/src/renderer/components/MainPanel.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; -import { Wand2, ExternalLink, Columns, Copy, Loader2, GitBranch, ArrowUp, ArrowDown, FileEdit, List, AlertCircle, X } from 'lucide-react'; +import { Wand2, ExternalLink, Columns, Copy, Loader2, GitBranch, ArrowUp, ArrowDown, FileEdit, List, AlertCircle, X, GitPullRequest, Settings2 } from 'lucide-react'; import { LogViewer } from './LogViewer'; import { TerminalOutput } from './TerminalOutput'; import { InputArea } from './InputArea'; @@ -182,6 +182,12 @@ interface MainPanelProps { showFlashNotification?: (message: string) => void; // Fuzzy file search callback (for FilePreview in preview mode) onOpenFuzzySearch?: () => void; + + // Worktree configuration + onOpenWorktreeConfig?: () => void; + onOpenCreatePR?: () => void; + /** True if this session is a worktree child (has parentSessionId) */ + isWorktreeChild?: boolean; } export const MainPanel = forwardRef(function MainPanel(props, ref) { @@ -204,7 +210,10 @@ export const MainPanel = forwardRef(function Ma handleInputKeyDown, handlePaste, handleDrop, getContextColor, setActiveSessionId, batchRunState, currentSessionBatchState, onStopBatchRun, showConfirmation, onRemoveQueuedItem, onOpenQueueBrowser, isMobileLandscape = false, - showFlashNotification + showFlashNotification, + onOpenWorktreeConfig, + onOpenCreatePR, + isWorktreeChild, } = props; // isCurrentSessionAutoMode: THIS session has active batch run (for all UI indicators) @@ -546,7 +555,7 @@ export const MainPanel = forwardRef(function Ma )} {/* Status Summary */} -
+
Status
{gitInfo.uncommittedChanges > 0 ? ( @@ -560,7 +569,41 @@ export const MainPanel = forwardRef(function Ma )}
-
+
+ + {/* Worktree Actions */} +
+ {/* Configure Worktrees - only for parent sessions (not worktree children) */} + {!isWorktreeChild && onOpenWorktreeConfig && ( + + )} + {/* Create PR - only for worktree children */} + {isWorktreeChild && onOpenCreatePR && ( + + )} +
diff --git a/src/renderer/components/QuickActionsModal.tsx b/src/renderer/components/QuickActionsModal.tsx index 6c0964f9..9ff785da 100644 --- a/src/renderer/components/QuickActionsModal.tsx +++ b/src/renderer/components/QuickActionsModal.tsx @@ -80,6 +80,8 @@ interface QuickActionsModalProps { hasActiveSessionCapability?: (capability: 'supportsSessionStorage' | 'supportsSlashCommands') => boolean; // Remote control onToggleRemoteControl?: () => void; + // Worktree PR creation + onOpenCreatePR?: (session: Session) => void; } export function QuickActionsModal(props: QuickActionsModalProps) { @@ -96,7 +98,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) { onRenameTab, onToggleReadOnlyMode, onOpenTabSwitcher, tabShortcuts, isAiMode, setPlaygroundOpen, onRefreshGitFileState, onDebugReleaseQueuedItem, markdownEditMode, onToggleMarkdownEditMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep, setDebugWizardModalOpen, setDebugPackageModalOpen, startTour, setFuzzyFileSearchOpen, onEditAgent, groupChats, onNewGroupChat, onOpenGroupChat, onCloseGroupChat, onDeleteGroupChat, activeGroupChatId, - hasActiveSessionCapability + hasActiveSessionCapability, onOpenCreatePR } = props; const [search, setSearch] = useState(''); @@ -295,6 +297,16 @@ export function QuickActionsModal(props: QuickActionsModalProps) { } setQuickActionOpen(false); } }] : []), + // Create PR - only for worktree child sessions + ...(activeSession && activeSession.parentSessionId && activeSession.worktreeBranch && onOpenCreatePR ? [{ + id: 'createPR', + label: `Create Pull Request: ${activeSession.worktreeBranch}`, + subtext: 'Open PR from this worktree branch', + action: () => { + onOpenCreatePR(activeSession); + setQuickActionOpen(false); + } + }] : []), ...(activeSession && onRefreshGitFileState ? [{ id: 'refreshGitFileState', label: 'Refresh Files, Git, History', subtext: 'Reload file tree, git status, and history', action: async () => { await onRefreshGitFileState(); setQuickActionOpen(false); diff --git a/src/renderer/components/SessionItem.tsx b/src/renderer/components/SessionItem.tsx index 17ff7070..e7cad8fa 100644 --- a/src/renderer/components/SessionItem.tsx +++ b/src/renderer/components/SessionItem.tsx @@ -13,8 +13,9 @@ import { getStatusColor } from '../utils/theme'; * - 'group': Session inside a group folder * - 'flat': Session in flat list (when no groups exist) * - 'ungrouped': Session in the Ungrouped folder (when groups exist) + * - 'worktree': Worktree child session nested under parent (shows branch name) */ -export type SessionItemVariant = 'bookmark' | 'group' | 'flat' | 'ungrouped'; +export type SessionItemVariant = 'bookmark' | 'group' | 'flat' | 'ungrouped' | 'worktree'; export interface SessionItemProps { session: Session; @@ -83,8 +84,8 @@ export function SessionItem({ onStartRename, onToggleBookmark, }: SessionItemProps) { - // Determine if we show the GIT/LOCAL badge (not shown in bookmark variant or terminal sessions) - const showGitLocalBadge = variant !== 'bookmark' && session.toolType !== 'terminal'; + // Determine if we show the GIT/LOCAL badge (not shown in bookmark variant, terminal sessions, or worktree variant) + const showGitLocalBadge = variant !== 'bookmark' && variant !== 'worktree' && session.toolType !== 'terminal'; // Determine container styling based on variant const getContainerClassName = () => { @@ -93,6 +94,10 @@ export function SessionItem({ if (variant === 'flat') { return `mx-3 px-3 py-2 rounded mb-1 ${base}`; } + if (variant === 'worktree') { + // Worktree children have extra left padding and smaller text + return `pl-8 pr-4 py-1.5 ${base}`; + } return `px-4 py-2 ${base}`; }; @@ -135,41 +140,47 @@ export function SessionItem({ {variant === 'bookmark' && session.bookmarked && ( )} + {/* Branch icon for worktree children */} + {variant === 'worktree' && ( + + )} - {session.name} + {variant === 'worktree' ? (session.worktreeBranch || session.name) : session.name} )} - {/* Session metadata row */} -
- {/* Session Jump Number Badge (Opt+Cmd+NUMBER) */} - {jumpNumber && ( -
- {jumpNumber} -
- )} - {session.toolType} + {/* Session metadata row (hidden for compact worktree variant) */} + {variant !== 'worktree' && ( +
+ {/* Session Jump Number Badge (Opt+Cmd+NUMBER) */} + {jumpNumber && ( +
+ {jumpNumber} +
+ )} + {session.toolType} - {/* Group badge (only in bookmark variant when session belongs to a group) */} - {variant === 'bookmark' && group && ( - - {group.name} - - )} -
+ {/* Group badge (only in bookmark variant when session belongs to a group) */} + {variant === 'bookmark' && group && ( + + {group.name} + + )} +
+ )} {/* Right side: Indicators and actions */} diff --git a/src/renderer/components/SessionList.tsx b/src/renderer/components/SessionList.tsx index e5f7c033..4a2c4de8 100644 --- a/src/renderer/components/SessionList.tsx +++ b/src/renderer/components/SessionList.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef } from 'react'; import { Wand2, Plus, Settings, ChevronRight, ChevronDown, X, Keyboard, Radio, Copy, ExternalLink, PanelLeftClose, PanelLeftOpen, Folder, Info, GitBranch, Bot, Clock, - ScrollText, Cpu, Menu, Bookmark, Trophy, Trash2, Edit3, FolderInput, Download, Compass, Globe + ScrollText, Cpu, Menu, Bookmark, Trophy, Trash2, Edit3, FolderInput, Download, Compass, Globe, + GitPullRequest } from 'lucide-react'; import { QRCodeSVG } from 'qrcode.react'; import type { Session, Group, Theme, Shortcut, AutoRunStats, GroupChat, GroupChatState, SettingsTab, FocusArea } from '../types'; @@ -30,6 +31,7 @@ interface SessionContextMenuProps { onMoveToGroup: (groupId: string) => void; onDelete: () => void; onDismiss: () => void; + onCreatePR?: () => void; // For worktree child sessions } function SessionContextMenu({ @@ -43,7 +45,8 @@ function SessionContextMenu({ onToggleBookmark, onMoveToGroup, onDelete, - onDismiss + onDismiss, + onCreatePR, }: SessionContextMenuProps) { const menuRef = useRef(null); const moveToGroupRef = useRef(null); @@ -144,6 +147,24 @@ function SessionContextMenu({ {session.bookmarked ? 'Remove Bookmark' : 'Add Bookmark'} + {/* Create PR - only for worktree child sessions */} + {session.parentSessionId && session.worktreeBranch && onCreatePR && ( + <> +
+ + + )} + {/* Divider */}
@@ -579,6 +600,10 @@ interface SessionListProps { // Edit agent modal handler (for context menu edit) onEditAgent: (session: Session) => void; + // Worktree handlers + onToggleWorktreeExpanded?: (sessionId: string) => void; + onOpenCreatePR?: (session: Session) => void; + // Auto mode props activeBatchSessionIds?: string[]; // Session IDs that are running in auto mode @@ -639,6 +664,8 @@ export function SessionList(props: SessionListProps) { onDeleteSession, onDeleteWorktreeGroup, setRenameInstanceModalOpen, setRenameInstanceValue, setRenameInstanceSessionId, onEditAgent, + onToggleWorktreeExpanded, + onOpenCreatePR, activeBatchSessionIds = [], showSessionJumpNumbers = false, visibleSessions = [], @@ -800,17 +827,149 @@ export function SessionList(props: SessionListProps) { gitFileCounts.set(sessionId, status.fileCount); }); + // Helper: Get worktree children for a parent session + const getWorktreeChildren = (parentId: string): Session[] => { + return sessions.filter(s => s.parentSessionId === parentId); + }; + + // Helper: Check if a session has worktree children + const hasWorktreeChildren = (sessionId: string): boolean => { + return sessions.some(s => s.parentSessionId === sessionId); + }; + + // Helper component: Renders a session item with its worktree children (if any) + const renderSessionWithWorktrees = ( + session: Session, + variant: 'bookmark' | 'group' | 'flat' | 'ungrouped', + options: { + keyPrefix: string; + groupId?: string; + group?: Group; + onDrop?: () => void; + } + ) => { + const worktreeChildren = getWorktreeChildren(session.id); + const hasWorktrees = worktreeChildren.length > 0; + const worktreesExpanded = session.worktreesExpanded ?? true; + const globalIdx = sortedSessions.findIndex(s => s.id === session.id); + const isKeyboardSelected = activeFocus === 'sidebar' && globalIdx === selectedSidebarIndex; + + return ( +
+ {/* Parent session with expand/collapse toggle for worktrees */} +
+ {/* Expand/collapse button for sessions with worktrees */} + {hasWorktrees && onToggleWorktreeExpanded && ( + + )} +
+ setActiveSessionId(session.id)} + onDragStart={() => handleDragStart(session.id)} + onDragOver={handleDragOver} + onDrop={options.onDrop || handleDropOnUngrouped} + onContextMenu={(e) => handleContextMenu(e, session.id)} + onFinishRename={(newName) => finishRenamingSession(session.id, newName)} + onStartRename={() => startRenamingSession(`${options.keyPrefix}-${session.id}`)} + onToggleBookmark={() => toggleBookmark(session.id)} + /> +
+ {/* Worktree count badge when collapsed */} + {hasWorktrees && !worktreesExpanded && ( +
1 ? 's' : ''}`} + > + + {worktreeChildren.length} +
+ )} +
+ + {/* Worktree children (when expanded) */} + {hasWorktrees && worktreesExpanded && ( +
+ {worktreeChildren.sort((a, b) => compareSessionNames(a.worktreeBranch || a.name, b.worktreeBranch || b.name)).map(child => { + const childGlobalIdx = sortedSessions.findIndex(s => s.id === child.id); + const isChildKeyboardSelected = activeFocus === 'sidebar' && childGlobalIdx === selectedSidebarIndex; + return ( + setActiveSessionId(child.id)} + onDragStart={() => handleDragStart(child.id)} + onContextMenu={(e) => handleContextMenu(e, child.id)} + onFinishRename={(newName) => finishRenamingSession(child.id, newName)} + onStartRename={() => startRenamingSession(`worktree-${session.id}-${child.id}`)} + onToggleBookmark={() => toggleBookmark(child.id)} + /> + ); + })} +
+ )} +
+ ); + }; + // Filter sessions based on search query (searches session name AND AI tab names) + // Also filters out worktree children (they're rendered under their parents) const filteredSessions = sessionFilter ? sessions.filter(s => { + // Exclude worktree children from main list (they appear under parent) + if (s.parentSessionId) return false; const query = sessionFilter.toLowerCase(); // Match session name if (s.name.toLowerCase().includes(query)) return true; // Match any AI tab name if (s.aiTabs?.some(tab => tab.name?.toLowerCase().includes(query))) return true; + // Match worktree children branch names + if (getWorktreeChildren(s.id).some(child => + child.worktreeBranch?.toLowerCase().includes(query) || + child.name.toLowerCase().includes(query) + )) return true; return false; }) - : sessions; + : sessions.filter(s => !s.parentSessionId); // Exclude worktree children from main list // When filter opens, apply filter mode preferences (or defaults on first open) // When filter closes, save current states as filter mode preferences and restore original states @@ -1490,32 +1649,11 @@ export function SessionList(props: SessionListProps) { {!bookmarksCollapsed ? (
{[...filteredSessions.filter(s => s.bookmarked)].sort((a, b) => compareSessionNames(a.name, b.name)).map(session => { - const globalIdx = sortedSessions.findIndex(s => s.id === session.id); - const isKeyboardSelected = activeFocus === 'sidebar' && globalIdx === selectedSidebarIndex; const group = groups.find(g => g.id === session.groupId); - return ( - setActiveSessionId(session.id)} - onDragStart={() => handleDragStart(session.id)} - onContextMenu={(e) => handleContextMenu(e, session.id)} - onFinishRename={(newName) => finishRenamingSession(session.id, newName)} - onStartRename={() => startRenamingSession(`bookmark-${session.id}`)} - onToggleBookmark={() => toggleBookmark(session.id)} - /> - ); + return renderSessionWithWorktrees(session, 'bookmark', { + keyPrefix: 'bookmark', + group, + }); })}
) : ( @@ -1641,35 +1779,13 @@ export function SessionList(props: SessionListProps) { {!group.collapsed ? (
- {groupSessions.map(session => { - const globalIdx = sortedSessions.findIndex(s => s.id === session.id); - const isKeyboardSelected = activeFocus === 'sidebar' && globalIdx === selectedSidebarIndex; - return ( - setActiveSessionId(session.id)} - onDragStart={() => handleDragStart(session.id)} - onDragOver={handleDragOver} - onDrop={() => handleDropOnGroup(group.id)} - onContextMenu={(e) => handleContextMenu(e, session.id)} - onFinishRename={(newName) => finishRenamingSession(session.id, newName)} - onStartRename={() => startRenamingSession(`group-${group.id}-${session.id}`)} - onToggleBookmark={() => toggleBookmark(session.id)} - /> - ); - })} + {groupSessions.map(session => + renderSessionWithWorktrees(session, 'group', { + keyPrefix: `group-${group.id}`, + groupId: group.id, + onDrop: () => handleDropOnGroup(group.id), + }) + )}
) : ( /* Collapsed Group Palette */ @@ -1732,34 +1848,9 @@ export function SessionList(props: SessionListProps) { {sessions.length > 0 && groups.length === 0 ? ( /* FLAT LIST - No groups exist yet, show sessions directly */
- {[...filteredSessions].sort((a, b) => compareSessionNames(a.name, b.name)).map((session) => { - const globalIdx = sortedSessions.findIndex(s => s.id === session.id); - const isKeyboardSelected = activeFocus === 'sidebar' && globalIdx === selectedSidebarIndex; - return ( - setActiveSessionId(session.id)} - onDragStart={() => handleDragStart(session.id)} - onDragOver={handleDragOver} - onDrop={handleDropOnUngrouped} - onContextMenu={(e) => handleContextMenu(e, session.id)} - onFinishRename={(newName) => finishRenamingSession(session.id, newName)} - onStartRename={() => startRenamingSession(`flat-${session.id}`)} - onToggleBookmark={() => toggleBookmark(session.id)} - /> - ); - })} + {[...filteredSessions].sort((a, b) => compareSessionNames(a.name, b.name)).map((session) => + renderSessionWithWorktrees(session, 'flat', { keyPrefix: 'flat' }) + )}
) : groups.length > 0 && ( /* UNGROUPED FOLDER - Groups exist, show as collapsible folder */ @@ -1795,34 +1886,9 @@ export function SessionList(props: SessionListProps) { {!ungroupedCollapsed ? (
- {[...filteredSessions.filter(s => !s.groupId)].sort((a, b) => compareSessionNames(a.name, b.name)).map((session) => { - const globalIdx = sortedSessions.findIndex(s => s.id === session.id); - const isKeyboardSelected = activeFocus === 'sidebar' && globalIdx === selectedSidebarIndex; - return ( - setActiveSessionId(session.id)} - onDragStart={() => handleDragStart(session.id)} - onDragOver={handleDragOver} - onDrop={handleDropOnUngrouped} - onContextMenu={(e) => handleContextMenu(e, session.id)} - onFinishRename={(newName) => finishRenamingSession(session.id, newName)} - onStartRename={() => startRenamingSession(`ungrouped-${session.id}`)} - onToggleBookmark={() => toggleBookmark(session.id)} - /> - ); - })} + {[...filteredSessions.filter(s => !s.groupId)].sort((a, b) => compareSessionNames(a.name, b.name)).map((session) => + renderSessionWithWorktrees(session, 'ungrouped', { keyPrefix: 'ungrouped' }) + )}
) : ( /* Collapsed Ungrouped Palette */ @@ -2020,6 +2086,7 @@ export function SessionList(props: SessionListProps) { onMoveToGroup={(groupId) => handleMoveToGroup(contextMenuSession.id, groupId)} onDelete={() => handleDeleteSession(contextMenuSession.id)} onDismiss={() => setContextMenu(null)} + onCreatePR={onOpenCreatePR && contextMenuSession.parentSessionId ? () => onOpenCreatePR(contextMenuSession) : undefined} /> )}
diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index 5ef49f02..454495d9 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -622,7 +622,7 @@ export function TabBar({ isDragging={draggingTabId === tab.id} isDragOver={dragOverTabId === tab.id} onRename={() => handleRenameRequest(tab.id)} - onStar={onTabStar ? (starred) => onTabStar(tab.id, starred) : undefined} + onStar={onTabStar && tab.agentSessionId ? (starred) => onTabStar(tab.id, starred) : undefined} onMarkUnread={onTabMarkUnread ? () => onTabMarkUnread(tab.id) : undefined} shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null} hasDraft={hasDraft(tab)} diff --git a/src/renderer/components/ThinkingStatusPill.tsx b/src/renderer/components/ThinkingStatusPill.tsx index 5fed71f6..57fdcb82 100644 --- a/src/renderer/components/ThinkingStatusPill.tsx +++ b/src/renderer/components/ThinkingStatusPill.tsx @@ -310,12 +310,13 @@ function ThinkingStatusPillInner({ sessions, theme, onSessionClick, namedSession // Use tab's agentSessionId if available, fallback to session's (legacy) const agentSessionId = writeModeTab?.agentSessionId || primarySession.agentSessionId; - // Priority: 1. namedSessions lookup, 2. tab's name, 3. session name, 4. UUID octet + // Priority: 1. namedSessions lookup, 2. tab's name, 3. UUID octet const customName = agentSessionId ? namedSessions?.[agentSessionId] : undefined; const tabName = writeModeTab?.name; - // Display name: prefer namedSessions, then tab name, then session name, then UUID octet - const displayClaudeId = customName || tabName || maestroSessionName || (agentSessionId ? agentSessionId.substring(0, 8).toUpperCase() : null); + // Display name for the tab slot (to the left of Stop button): + // prefer namedSessions, then tab name, then UUID octet (NOT session name - that's already shown) + const displayClaudeId = customName || tabName || (agentSessionId ? agentSessionId.substring(0, 8).toUpperCase() : null); // For tooltip, show all available info const tooltipParts = [maestroSessionName]; diff --git a/src/renderer/constants/modalPriorities.ts b/src/renderer/constants/modalPriorities.ts index 25ab12cc..d8d77263 100644 --- a/src/renderer/constants/modalPriorities.ts +++ b/src/renderer/constants/modalPriorities.ts @@ -71,6 +71,12 @@ export const MODAL_PRIORITIES = { /** Onboarding wizard - high priority, guides new users through setup */ WIZARD: 760, + /** Create PR modal (from worktree) */ + CREATE_PR: 755, + + /** Worktree configuration modal */ + WORKTREE_CONFIG: 752, + /** New instance creation modal */ NEW_INSTANCE: 750, diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index ab425f13..973785e4 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -279,6 +279,27 @@ interface MaestroAPI { branch?: string; error?: string; }>; + checkGhCli: (ghPath?: string) => Promise<{ + installed: boolean; + authenticated: boolean; + }>; + listWorktrees: (cwd: string) => Promise<{ + worktrees: Array<{ + path: string; + head: string; + branch: string | null; + isBare: boolean; + }>; + }>; + scanWorktreeDirectory: (parentPath: string) => Promise<{ + gitSubdirs: Array<{ + path: string; + name: string; + isWorktree: boolean; + branch: string | null; + repoRoot: string | null; + }>; + }>; }; fs: { homeDir: () => Promise; diff --git a/src/renderer/hooks/usePlaybookManagement.ts b/src/renderer/hooks/usePlaybookManagement.ts index 130c868c..25b89020 100644 --- a/src/renderer/hooks/usePlaybookManagement.ts +++ b/src/renderer/hooks/usePlaybookManagement.ts @@ -15,7 +15,9 @@ * - sessionId: For playbook storage scope * - folderPath: For export/import operations * - allDocuments: For detecting missing documents when loading playbooks - * - Current configuration state (documents, loop, prompt, worktree) for modification detection + * - Current configuration state (documents, loop, prompt) for modification detection + * + * Note: Worktree configuration has been moved to WorktreeConfigModal (git branch overlay) */ import { generateId } from '../utils/ids'; @@ -29,16 +31,13 @@ import type { /** * Configuration passed to the hook for modification detection + * Note: Worktree configuration has been moved to WorktreeConfigModal */ export interface PlaybookConfigState { documents: BatchDocumentEntry[]; loopEnabled: boolean; maxLoops: number | null; prompt: string; - worktreeEnabled: boolean; - branchName: string; - createPROnCompletion: boolean; - prTargetBranch: string; } /** @@ -141,7 +140,7 @@ export function usePlaybookManagement( const isPlaybookModified = useMemo(() => { if (!loadedPlaybook) return false; - const { documents, loopEnabled, maxLoops, prompt, worktreeEnabled, branchName, createPROnCompletion, prTargetBranch } = config; + const { documents, loopEnabled, maxLoops, prompt } = config; // Compare documents const currentDocs = documents.map((d) => ({ @@ -170,20 +169,6 @@ export function usePlaybookManagement( // Compare prompt if (prompt !== loadedPlaybook.prompt) return true; - // Compare worktree settings - const savedWorktree = loadedPlaybook.worktreeSettings; - if (savedWorktree) { - // Playbook has worktree settings - check if current state differs - if (!worktreeEnabled) return true; - if (branchName !== savedWorktree.branchNameTemplate) return true; - if (createPROnCompletion !== savedWorktree.createPROnCompletion) return true; - if (savedWorktree.prTargetBranch && prTargetBranch !== savedWorktree.prTargetBranch) - return true; - } else { - // Playbook doesn't have worktree settings - modified if worktree is now enabled with a branch - if (worktreeEnabled && branchName) return true; - } - return false; }, [config, loadedPlaybook]); @@ -205,15 +190,12 @@ export function usePlaybookManagement( })); // Apply configuration through callback + // Note: Worktree settings are no longer managed here - see WorktreeConfigModal onApplyPlaybook({ documents: entries, loopEnabled: playbook.loopEnabled, maxLoops: playbook.maxLoops ?? null, prompt: playbook.prompt, - worktreeEnabled: !!playbook.worktreeSettings, - branchName: playbook.worktreeSettings?.branchNameTemplate ?? '', - createPROnCompletion: playbook.worktreeSettings?.createPROnCompletion ?? false, - prTargetBranch: playbook.worktreeSettings?.prTargetBranch ?? 'main', }); setLoadedPlaybook(playbook); @@ -295,9 +277,10 @@ export function usePlaybookManagement( setSavingPlaybook(true); try { - const { documents, loopEnabled, maxLoops, prompt, worktreeEnabled, branchName, createPROnCompletion, prTargetBranch } = config; + const { documents, loopEnabled, maxLoops, prompt } = config; - // Build playbook data, including worktree settings if enabled + // Build playbook data + // Note: Worktree settings are no longer stored in playbooks - see WorktreeConfigModal const playbookData: Parameters[1] = { name, documents: documents.map((d) => ({ @@ -309,16 +292,6 @@ export function usePlaybookManagement( prompt, }; - // Include worktree settings if worktree is enabled - // Note: We store branchName as the template - users can modify it when loading - if (worktreeEnabled && branchName) { - playbookData.worktreeSettings = { - branchNameTemplate: branchName, - createPROnCompletion, - prTargetBranch, - }; - } - const result = await window.maestro.playbooks.create(sessionId, playbookData); if (result.success) { @@ -340,9 +313,10 @@ export function usePlaybookManagement( setSavingPlaybook(true); try { - const { documents, loopEnabled, maxLoops, prompt, worktreeEnabled, branchName, createPROnCompletion, prTargetBranch } = config; + const { documents, loopEnabled, maxLoops, prompt } = config; - // Build update data, including worktree settings if enabled + // Build update data + // Note: Worktree settings are no longer stored in playbooks - see WorktreeConfigModal const updateData: Parameters[2] = { documents: documents.map((d) => ({ filename: d.filename, @@ -354,18 +328,6 @@ export function usePlaybookManagement( updatedAt: Date.now(), }; - // Include worktree settings if worktree is enabled, otherwise clear them - if (worktreeEnabled && branchName) { - updateData.worktreeSettings = { - branchNameTemplate: branchName, - createPROnCompletion, - prTargetBranch, - }; - } else { - // Explicitly set to undefined to clear previous worktree settings - updateData.worktreeSettings = undefined; - } - const result = await window.maestro.playbooks.update(sessionId, loadedPlaybook.id, updateData); if (result.success) { diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index da778231..f8a7a6dc 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -329,7 +329,18 @@ export interface Session { gitBranches?: string[]; gitTags?: string[]; gitRefsCacheTime?: number; // Timestamp when branches/tags were last fetched - // Worktree parent path - if set, this session is a worktree parent that should be scanned for new worktrees + // Worktree configuration (only set on parent sessions that manage worktrees) + worktreeConfig?: { + basePath: string; // Directory where worktrees are stored + watchEnabled: boolean; // Whether to watch for new worktrees via chokidar + }; + // Worktree child indicator (only set on worktree child sessions) + parentSessionId?: string; // Links back to parent agent session + worktreeBranch?: string; // The git branch this worktree is checked out to + // Whether worktree children are expanded in the sidebar (only on parent sessions) + worktreesExpanded?: boolean; + // Legacy: Worktree parent path for auto-discovery (will be migrated to worktreeConfig) + // TODO: Remove after migration to new parent/child model worktreeParentPath?: string; // File Explorer per-session state fileTree: any[];