Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DmuxApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const DmuxApp: React.FC<DmuxAppProps> = ({

// Agent selection state
const { availableAgents } = useAgentDetection()
const [agentChoice, setAgentChoice] = useState<"claude" | "opencode" | null>(
const [agentChoice, setAgentChoice] = useState<"claude" | "opencode" | "vibe" | null>(
null
)

Expand Down
10 changes: 5 additions & 5 deletions src/actions/merge/conflictResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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) => {
Expand All @@ -51,7 +51,7 @@ export async function createConflictResolutionPaneForMerge(
context,
targetBranch,
targetRepoPath,
agentId as 'claude' | 'opencode'
agentId as 'claude' | 'opencode' | 'vibe'
);
},
dismissable: true,
Expand All @@ -76,7 +76,7 @@ async function createAndLaunchConflictPane(
context: ActionContext,
targetBranch: string,
targetRepoPath: string,
agent: 'claude' | 'opencode'
agent: 'claude' | 'opencode' | 'vibe'
): Promise<ActionResult> {
try {
const { createConflictResolutionPane } = await import('../../utils/conflictResolutionPane.js');
Expand Down
7 changes: 7 additions & 0 deletions src/assets/mistral.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/components/dialogs/AgentChoiceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentChoiceDialogProps> = ({ agentChoice }) => {
Expand All @@ -17,6 +17,9 @@ const AgentChoiceDialog: React.FC<AgentChoiceDialogProps> = ({ agentChoice }) =>
<Text color={agentChoice === 'opencode' ? 'cyan' : 'white'}>
{agentChoice === 'opencode' ? '▶ opencode' : ' opencode'}
</Text>
<Text color={agentChoice === 'vibe' ? 'cyan' : 'white'}>
{agentChoice === 'vibe' ? '▶ Mistral Vibe' : ' Mistral Vibe'}
</Text>
</Box>
</Box>
</Box>
Expand Down
2 changes: 1 addition & 1 deletion src/components/panes/PaneCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const PaneCard: React.FC<PaneCardProps> = memo(({ pane, selected, isFirstPane, i
{pane.type === 'shell' ? (
<Text color="cyan"> [{pane.shellType || 'shell'}]</Text>
) : pane.agent && (
<Text color="gray"> [{pane.agent === 'claude' ? 'cc' : 'oc'}]</Text>
<Text color="gray"> [{pane.agent === 'claude' ? 'cc' : pane.agent === 'opencode' ? 'oc' : 'mv'}]</Text>
)}
{pane.autopilot && (
<Text color={COLORS.success}> (ap)</Text>
Expand Down
14 changes: 9 additions & 5 deletions src/components/popups/agentChoicePopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentChoicePopupProps> = ({
Expand Down Expand Up @@ -42,6 +42,10 @@ const AgentChoicePopupApp: React.FC<AgentChoicePopupProps> = ({
// 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);
Expand All @@ -55,7 +59,7 @@ const AgentChoicePopupApp: React.FC<AgentChoicePopupProps> = ({
<Box flexDirection="column">
{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 (
<Box key={agent} marginBottom={index < availableAgents.length - 1 ? 1 : 0}>
<Text
Expand All @@ -78,14 +82,14 @@ const AgentChoicePopupApp: React.FC<AgentChoicePopupProps> = ({
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) {
Expand Down
33 changes: 31 additions & 2 deletions src/hooks/useAgentDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { execSync } from 'child_process';
import fs from 'fs/promises';

export default function useAgentDetection() {
const [availableAgents, setAvailableAgents] = useState<Array<'claude' | 'opencode'>>([]);
const [availableAgents, setAvailableAgents] = useState<Array<'claude' | 'opencode' | 'vibe'>>([]);

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 {}
})();
Expand Down Expand Up @@ -75,3 +77,30 @@ const findopencodeCommand = async (): Promise<string | null> => {

return null;
};

const findVibeCommand = async (): Promise<string | null> => {
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;
};
4 changes: 2 additions & 2 deletions src/hooks/usePaneCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Params {
setStatusMessage: (msg: string) => void;
loadPanes: () => Promise<void>;
panesFile: string;
availableAgents: Array<'claude' | 'opencode'>;
availableAgents: Array<'claude' | 'opencode' | 'vibe'>;
forceRepaint?: () => void;
}

Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/server/routes/panesRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ 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"' };
}

// 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 () => {
Expand Down
6 changes: 3 additions & 3 deletions src/services/PopupManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
38 changes: 35 additions & 3 deletions src/utils/agentDetection.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -69,14 +69,46 @@ export async function findOpencodeCommand(): Promise<string | null> {
return null;
}

/**
* Find Mistral Vibe CLI command
*/
export async function findVibeCommand(): Promise<string | null> {
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<Array<'claude' | 'opencode'>> {
const agents: Array<'claude' | 'opencode'> = [];
export async function getAvailableAgents(): Promise<Array<'claude' | 'opencode' | 'vibe'>> {
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;
}
13 changes: 12 additions & 1 deletion src/utils/conflictResolutionPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/utils/paneCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,7 +35,7 @@ export interface CreatePaneResult {
*/
export async function createPane(
options: CreatePaneOptions,
availableAgents: Array<'claude' | 'opencode'>
availableAgents: Array<'claude' | 'opencode' | 'vibe'>
): Promise<CreatePaneResult> {
const { prompt, projectName, existingPanes } = options;
let { agent, projectRoot: optionsProjectRoot } = options;
Expand Down Expand Up @@ -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
Expand Down
Loading