diff --git a/src/DmuxApp.tsx b/src/DmuxApp.tsx index c6676ef..38fcf18 100644 --- a/src/DmuxApp.tsx +++ b/src/DmuxApp.tsx @@ -136,7 +136,7 @@ const DmuxApp: React.FC = ({ // Agent selection state const { availableAgents } = useAgentDetection() - const [agentChoice, setAgentChoice] = useState<"claude" | "opencode" | null>( + const [agentChoice, setAgentChoice] = useState<"claude" | "opencode" | "vibe" | null>( null ) diff --git a/src/actions/merge/conflictResolution.ts b/src/actions/merge/conflictResolution.ts index 48ca082..8a87220 100644 --- a/src/actions/merge/conflictResolution.ts +++ b/src/actions/merge/conflictResolution.ts @@ -21,7 +21,7 @@ export async function createConflictResolutionPaneForMerge( // First, check which agents are available const { findClaudeCommand, findOpencodeCommand } = await import('../../utils/agentDetection.js'); - const availableAgents: Array<'claude' | 'opencode'> = []; + const availableAgents: Array<'claude' | 'opencode' | 'vibe'> = []; if (await findClaudeCommand()) availableAgents.push('claude'); if (await findOpencodeCommand()) availableAgents.push('opencode'); @@ -41,8 +41,8 @@ export async function createConflictResolutionPaneForMerge( message: 'Which agent would you like to use to resolve merge conflicts?', options: availableAgents.map(agent => ({ id: agent, - label: agent === 'claude' ? 'Claude Code' : 'OpenCode', - description: agent === 'claude' ? 'Anthropic Claude' : 'Open-source alternative', + label: agent === 'claude' ? 'Claude Code' : agent === 'opencode' ? 'OpenCode' : 'Mistral Vibe', + description: agent === 'claude' ? 'Anthropic Claude' : agent === 'opencode' ? 'Open-source alternative' : 'Vibe by Mistral AI', default: agent === 'claude', })), onSelect: async (agentId: string) => { @@ -51,7 +51,7 @@ export async function createConflictResolutionPaneForMerge( context, targetBranch, targetRepoPath, - agentId as 'claude' | 'opencode' + agentId as 'claude' | 'opencode' | 'vibe' ); }, dismissable: true, @@ -76,7 +76,7 @@ async function createAndLaunchConflictPane( context: ActionContext, targetBranch: string, targetRepoPath: string, - agent: 'claude' | 'opencode' + agent: 'claude' | 'opencode' | 'vibe' ): Promise { try { const { createConflictResolutionPane } = await import('../../utils/conflictResolutionPane.js'); diff --git a/src/assets/mistral.svg b/src/assets/mistral.svg new file mode 100644 index 0000000..fdbf183 --- /dev/null +++ b/src/assets/mistral.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/dialogs/AgentChoiceDialog.tsx b/src/components/dialogs/AgentChoiceDialog.tsx index 215fdf0..effe5b2 100644 --- a/src/components/dialogs/AgentChoiceDialog.tsx +++ b/src/components/dialogs/AgentChoiceDialog.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Box, Text } from 'ink'; interface AgentChoiceDialogProps { - agentChoice: 'claude' | 'opencode' | null; + agentChoice: 'claude' | 'opencode' | 'vibe' | null; } const AgentChoiceDialog: React.FC = ({ agentChoice }) => { @@ -17,6 +17,9 @@ const AgentChoiceDialog: React.FC = ({ agentChoice }) => {agentChoice === 'opencode' ? '▶ opencode' : ' opencode'} + + {agentChoice === 'vibe' ? '▶ Mistral Vibe' : ' Mistral Vibe'} + diff --git a/src/components/panes/PaneCard.tsx b/src/components/panes/PaneCard.tsx index 73b188e..b6116d7 100644 --- a/src/components/panes/PaneCard.tsx +++ b/src/components/panes/PaneCard.tsx @@ -49,7 +49,7 @@ const PaneCard: React.FC = memo(({ pane, selected, isFirstPane, i {pane.type === 'shell' ? ( [{pane.shellType || 'shell'}] ) : pane.agent && ( - [{pane.agent === 'claude' ? 'cc' : 'oc'}] + [{pane.agent === 'claude' ? 'cc' : pane.agent === 'opencode' ? 'oc' : 'mv'}] )} {pane.autopilot && ( (ap) diff --git a/src/components/popups/agentChoicePopup.tsx b/src/components/popups/agentChoicePopup.tsx index 4699980..10760bb 100755 --- a/src/components/popups/agentChoicePopup.tsx +++ b/src/components/popups/agentChoicePopup.tsx @@ -12,8 +12,8 @@ import { PopupFooters, POPUP_CONFIG } from './config.js'; interface AgentChoicePopupProps { resultFile: string; - availableAgents: Array<'claude' | 'opencode'>; - defaultAgent?: 'claude' | 'opencode'; + availableAgents: Array<'claude' | 'opencode' | 'vibe'>; + defaultAgent?: 'claude' | 'opencode' | 'vibe'; } const AgentChoicePopupApp: React.FC = ({ @@ -42,6 +42,10 @@ const AgentChoicePopupApp: React.FC = ({ // Find opencode index const opencodeIdx = availableAgents.indexOf('opencode'); if (opencodeIdx >= 0) setSelectedIndex(opencodeIdx); + } else if (input === '3' || input.toLowerCase() === 'm') { + // Find vibe index + const vibeIdx = availableAgents.indexOf('vibe'); + if (vibeIdx >= 0) setSelectedIndex(vibeIdx); } else if (key.return) { // User confirmed choice writeSuccessAndExit(resultFile, selectedAgent, exit); @@ -55,7 +59,7 @@ const AgentChoicePopupApp: React.FC = ({ {availableAgents.map((agent, index) => { const isSelected = index === selectedIndex; - const label = agent === 'claude' ? 'Claude Code' : 'opencode'; + const label = agent === 'claude' ? 'Claude Code' : agent === 'opencode' ? 'OpenCode' : 'Mistral Vibe'; return ( = ({ function main() { const resultFile = process.argv[2]; const agentsJson = process.argv[3]; - const defaultAgent = process.argv[4] as 'claude' | 'opencode' | undefined; + const defaultAgent = process.argv[4] as 'claude' | 'opencode' | 'vibe' | undefined; if (!resultFile || !agentsJson) { console.error('Error: Result file and agents JSON required'); process.exit(1); } - let availableAgents: Array<'claude' | 'opencode'>; + let availableAgents: Array<'claude' | 'opencode' | 'vibe'>; try { availableAgents = JSON.parse(agentsJson); } catch (error) { diff --git a/src/hooks/useAgentDetection.ts b/src/hooks/useAgentDetection.ts index edb519d..4e4b05c 100644 --- a/src/hooks/useAgentDetection.ts +++ b/src/hooks/useAgentDetection.ts @@ -3,16 +3,18 @@ import { execSync } from 'child_process'; import fs from 'fs/promises'; export default function useAgentDetection() { - const [availableAgents, setAvailableAgents] = useState>([]); + const [availableAgents, setAvailableAgents] = useState>([]); useEffect(() => { (async () => { try { - const agents: Array<'claude' | 'opencode'> = []; + const agents: Array<'claude' | 'opencode' | 'vibe'> = []; const hasClaude = await findClaudeCommand(); if (hasClaude) agents.push('claude'); const hasopencode = await findopencodeCommand(); if (hasopencode) agents.push('opencode'); + const hasVibe = await findVibeCommand(); + if (hasVibe) agents.push('vibe'); setAvailableAgents(agents); } catch {} })(); @@ -75,3 +77,30 @@ const findopencodeCommand = async (): Promise => { return null; }; + +const findVibeCommand = async (): Promise => { + try { + const userShell = process.env.SHELL || '/bin/bash'; + const result = execSync( + `${userShell} -i -c "command -v vibe 2>/dev/null || which vibe 2>/dev/null"`, + { encoding: 'utf-8', stdio: 'pipe' } + ).trim(); + if (result) return result.split('\n')[0]; + } catch {} + + const commonPaths = [ + `${process.env.HOME}/.local/bin/vibe`, + '/usr/local/bin/vibe', + '/opt/homebrew/bin/vibe', + '/usr/bin/vibe', + `${process.env.HOME}/bin/vibe`, + ]; + for (const p of commonPaths) { + try { + await fs.access(p); + return p; + } catch {} + } + + return null; +}; diff --git a/src/hooks/usePaneCreation.ts b/src/hooks/usePaneCreation.ts index 61f3e2d..b4a59c7 100644 --- a/src/hooks/usePaneCreation.ts +++ b/src/hooks/usePaneCreation.ts @@ -12,7 +12,7 @@ interface Params { setStatusMessage: (msg: string) => void; loadPanes: () => Promise; panesFile: string; - availableAgents: Array<'claude' | 'opencode'>; + availableAgents: Array<'claude' | 'opencode' | 'vibe'>; forceRepaint?: () => void; } @@ -38,7 +38,7 @@ export default function usePaneCreation({ panes, savePanes, projectName, setIsCr } catch {} }; - const createNewPane = async (prompt: string, agent?: 'claude' | 'opencode') => { + const createNewPane = async (prompt: string, agent?: 'claude' | 'opencode' | 'vibe') => { // CRITICAL: Force repaint FIRST to prevent blank screen if (forceRepaint) { forceRepaint(); diff --git a/src/hooks/useServices.ts b/src/hooks/useServices.ts index 73dc8cf..8b2b47e 100644 --- a/src/hooks/useServices.ts +++ b/src/hooks/useServices.ts @@ -9,8 +9,8 @@ interface UseServicesProps { popupsSupported: boolean terminalWidth: number terminalHeight: number - availableAgents: Array<"claude" | "opencode"> - agentChoice: "claude" | "opencode" | null + availableAgents: Array<"claude" | "opencode" | "vibe"> + agentChoice: "claude" | "opencode" | "vibe" | null serverPort?: number server?: any settingsManager: any diff --git a/src/server/routes/panesRoutes.ts b/src/server/routes/panesRoutes.ts index e37170f..d22bb61 100644 --- a/src/server/routes/panesRoutes.ts +++ b/src/server/routes/panesRoutes.ts @@ -86,7 +86,7 @@ export function createPanesRoutes() { console.error('[API] After normalization, agent =', agent); - if (agent && agent !== 'claude' && agent !== 'opencode') { + if (agent && agent !== 'claude' && agent !== 'opencode' && agent !== 'vibe') { event.node.res.statusCode = 400; return { error: 'Invalid agent. Must be "claude" or "opencode"' }; } @@ -94,7 +94,7 @@ export function createPanesRoutes() { // Get available agents using robust detection (same as TUI) const { execSync } = await import('child_process'); const fsPromises = await import('fs/promises'); - const availableAgents: Array<'claude' | 'opencode'> = []; + const availableAgents: Array<'claude' | 'opencode' | 'vibe'> = []; // Check for Claude const hasClaude = await (async () => { diff --git a/src/services/PopupManager.ts b/src/services/PopupManager.ts index 4d314c1..a1a2272 100644 --- a/src/services/PopupManager.ts +++ b/src/services/PopupManager.ts @@ -24,8 +24,8 @@ export interface PopupManagerConfig { popupsSupported: boolean terminalWidth: number terminalHeight: number - availableAgents: Array<"claude" | "opencode"> - agentChoice: "claude" | "opencode" | null + availableAgents: Array<"claude" | "opencode" | "vibe"> + agentChoice: "claude" | "opencode" | "vibe" | null serverPort?: number server?: any settingsManager: any @@ -287,7 +287,7 @@ export class PopupManager { try { const agentsJson = JSON.stringify(this.config.availableAgents) const defaultAgent = - this.config.agentChoice || this.config.availableAgents[0] || "claude" + this.config.agentChoice || this.config.availableAgents[0] || "vibe" const result = await this.launchPopup<"claude" | "opencode">( "agentChoicePopup.js", diff --git a/src/types.ts b/src/types.ts index 0c8c5e4..0e36653 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,7 +26,7 @@ export interface DmuxPane { devWindowId?: string; // Background window for dev server devStatus?: 'running' | 'stopped'; devUrl?: string; // Detected dev server URL - agent?: 'claude' | 'opencode'; + agent?: 'claude' | 'opencode' | 'vibe'; agentStatus?: AgentStatus; // Agent working/attention status lastAgentCheck?: number; // Timestamp of last status check lastDeterministicStatus?: 'ambiguous' | 'working'; // For LLM detection coordination @@ -67,7 +67,7 @@ export interface DmuxSettings { // Autopilot settings enableAutopilotByDefault?: boolean; // Agent selection - defaultAgent?: 'claude' | 'opencode'; + defaultAgent?: 'claude' | 'opencode' | 'vibe'; // Tmux hooks for event-driven updates (low CPU) // true = use hooks, false = use polling, undefined = not yet asked useTmuxHooks?: boolean; diff --git a/src/utils/agentDetection.ts b/src/utils/agentDetection.ts index 899c002..8c4dad8 100644 --- a/src/utils/agentDetection.ts +++ b/src/utils/agentDetection.ts @@ -1,7 +1,7 @@ /** * Agent Detection Utilities * - * Utilities to detect available AI agents (claude, opencode) + * Utilities to detect available AI agents (claude, opencode, vibe) */ import { execSync } from 'child_process'; @@ -69,14 +69,46 @@ export async function findOpencodeCommand(): Promise { return null; } +/** + * Find Mistral Vibe CLI command + */ +export async function findVibeCommand(): Promise { + try { + const userShell = process.env.SHELL || '/bin/bash'; + const result = execSync( + `${userShell} -i -c "command -v vibe 2>/dev/null || which vibe 2>/dev/null"`, + { encoding: 'utf-8', stdio: 'pipe' } + ).trim(); + if (result) return result.split('\n')[0]; + } catch {} + + const commonPaths = [ + `${process.env.HOME}/.local/bin/vibe`, + '/usr/local/bin/vibe', + '/opt/homebrew/bin/vibe', + '/usr/bin/vibe', + `${process.env.HOME}/bin/vibe`, + ]; + + for (const p of commonPaths) { + try { + await fs.access(p); + return p; + } catch {} + } + + return null; +} + /** * Get all available agents */ -export async function getAvailableAgents(): Promise> { - const agents: Array<'claude' | 'opencode'> = []; +export async function getAvailableAgents(): Promise> { + const agents: Array<'claude' | 'opencode' | 'vibe'> = []; if (await findClaudeCommand()) agents.push('claude'); if (await findOpencodeCommand()) agents.push('opencode'); + if (await findVibeCommand()) agents.push('vibe'); return agents; } diff --git a/src/utils/conflictResolutionPane.ts b/src/utils/conflictResolutionPane.ts index cd8119b..6e9e417 100644 --- a/src/utils/conflictResolutionPane.ts +++ b/src/utils/conflictResolutionPane.ts @@ -15,7 +15,7 @@ export interface ConflictResolutionPaneOptions { sourceBranch: string; // Branch being merged (the worktree branch) targetBranch: string; // Branch merging into (usually main) targetRepoPath: string; // Path to the target repository (where merge will happen) - agent: 'claude' | 'opencode'; + agent: 'claude' | 'opencode' | 'vibe'; projectName: string; existingPanes: DmuxPane[]; } @@ -127,6 +127,17 @@ export async function createConflictResolutionPane( await new Promise((resolve) => setTimeout(resolve, 200)); await tmuxService.deleteBuffer(bufName); await tmuxService.sendTmuxKeys(paneInfo, 'Enter'); + } else if (agent === 'vibe') { + // Mistral Vibe conflict resolution + const escapedPrompt = prompt + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/`/g, '\\`') + .replace(/\$/g, '\\\$'); + const vibeCmd = `vibe "${escapedPrompt}"`; + + await tmuxService.sendShellCommand(paneInfo, vibeCmd); + await tmuxService.sendTmuxKeys(paneInfo, 'Enter'); } // Keep focus on the new pane diff --git a/src/utils/paneCreation.ts b/src/utils/paneCreation.ts index 9de5ee9..55fa619 100644 --- a/src/utils/paneCreation.ts +++ b/src/utils/paneCreation.ts @@ -18,7 +18,7 @@ import { atomicWriteJsonSync } from './atomicWrite.js'; export interface CreatePaneOptions { prompt: string; - agent?: 'claude' | 'opencode'; + agent?: 'claude' | 'opencode' | 'vibe'; projectName: string; existingPanes: DmuxPane[]; projectRoot?: string; @@ -35,7 +35,7 @@ export interface CreatePaneResult { */ export async function createPane( options: CreatePaneOptions, - availableAgents: Array<'claude' | 'opencode'> + availableAgents: Array<'claude' | 'opencode' | 'vibe'> ): Promise { const { prompt, projectName, existingPanes } = options; let { agent, projectRoot: optionsProjectRoot } = options; @@ -386,6 +386,20 @@ export async function createPane( await tmuxService.deleteBuffer(bufName); await tmuxService.sendTmuxKeys(paneInfo, 'Enter'); } + } else if (agent === 'vibe') { + // Mistral Vibe command execution + if (prompt && prompt.trim()) { + const escapedPrompt = prompt + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/`/g, '\\`') + .replace(/\$/g, '\\\$'); + const vibeCmd = `vibe "${escapedPrompt}"`; + await tmuxService.sendShellCommand(paneInfo, vibeCmd); + } else { + await tmuxService.sendShellCommand(paneInfo, 'vibe'); + } + await tmuxService.sendTmuxKeys(paneInfo, 'Enter'); } // Keep focus on the new pane diff --git a/src/utils/settingsManager.ts b/src/utils/settingsManager.ts index b6d999b..540d282 100644 --- a/src/utils/settingsManager.ts +++ b/src/utils/settingsManager.ts @@ -21,6 +21,7 @@ export const SETTING_DEFINITIONS: SettingDefinition[] = [ { value: '', label: 'Ask each time' }, { value: 'claude', label: 'Claude Code' }, { value: 'opencode', label: 'OpenCode' }, + { value: 'vibe', label: 'Mistral Vibe' }, ], }, { diff --git a/src/utils/systemCheck.ts b/src/utils/systemCheck.ts index c99b9df..d3e3109 100644 --- a/src/utils/systemCheck.ts +++ b/src/utils/systemCheck.ts @@ -149,9 +149,9 @@ export async function validateSystemRequirements(): Promise { // Warnings for missing agents if (checks.agents.length === 0) { - warnings.push('No agents found (claude or opencode). You will not be able to use AI features.'); + warnings.push('No agents found (claude, opencode, or vibe). You will not be able to use AI features.'); } else if (checks.agents.length === 1) { - const allAgents: Array<'claude' | 'opencode'> = ['claude', 'opencode']; + const allAgents: Array<'claude' | 'opencode' | 'vibe'> = ['claude', 'opencode', 'vibe']; const missing = allAgents.find(a => !checks.agents.includes(a)); warnings.push(`Agent '${missing}' not found. Only '${checks.agents[0]}' is available.`); } diff --git a/src/workers/PaneWorker.ts b/src/workers/PaneWorker.ts index b3b8d73..4e5d3aa 100644 --- a/src/workers/PaneWorker.ts +++ b/src/workers/PaneWorker.ts @@ -14,7 +14,7 @@ import type { class PaneWorker { private paneId: string; private tmuxPaneId: string; - private agent?: 'claude' | 'opencode'; + private agent?: 'claude' | 'opencode' | 'vibe'; private captureHistory: string[] = []; private pollInterval: NodeJS.Timeout | null = null; private pollIntervalMs: number; @@ -352,6 +352,16 @@ class PaneWorker { /⏳.*processing/i ]; return opencodeWorkingPatterns.some(pattern => pattern.test(content)); + } else if (this.agent === 'vibe') { + // Mistral Vibe specific working indicators + const vibeWorkingPatterns = [ + /working\.\.\./i, + /processing\.\.\./i, + /thinking\.\.\./i, + /⏳.*working/i, + /⏳.*processing/i + ]; + return vibeWorkingPatterns.some(pattern => pattern.test(content)); } return false; diff --git a/src/workers/WorkerMessages.ts b/src/workers/WorkerMessages.ts index 82afcbf..85ef943 100644 --- a/src/workers/WorkerMessages.ts +++ b/src/workers/WorkerMessages.ts @@ -22,7 +22,7 @@ export interface OutboundMessage extends WorkerMessage { export interface WorkerConfig { paneId: string; tmuxPaneId: string; - agent?: 'claude' | 'opencode'; + agent?: 'claude' | 'opencode' | 'vibe'; pollInterval?: number; // Default 1000ms }