diff --git a/src/PixelAgentsViewProvider.ts b/src/PixelAgentsViewProvider.ts index fe1cdd81..59f804b0 100644 --- a/src/PixelAgentsViewProvider.ts +++ b/src/PixelAgentsViewProvider.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { + getAllProjectDirPaths, getProjectDirPath, launchNewTerminal, persistAgents, @@ -77,7 +78,7 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { webviewView.webview.html = getWebviewContent(webviewView.webview, this.extensionUri); webviewView.webview.onDidReceiveMessage(async (message) => { - if (message.type === 'openClaude') { + if (message.type === 'openAgent') { await launchNewTerminal( this.nextAgentId, this.nextTerminalIndex, @@ -92,6 +93,7 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { this.projectScanTimer, this.webview, this.persistAgents, + message.agentType as string, message.folderPath as string | undefined, ); } else if (message.type === 'focusAgent') { @@ -144,13 +146,13 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { } // Ensure project scan runs even with no restored agents (to adopt external terminals) - const projectDir = getProjectDirPath(); + const projectDirs = getAllProjectDirPaths(); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; console.log('[Extension] workspaceRoot:', workspaceRoot); - console.log('[Extension] projectDir:', projectDir); - if (projectDir) { + console.log('[Extension] projectDirs:', projectDirs); + if (projectDirs.length > 0) { ensureProjectScan( - projectDir, + projectDirs, this.knownJsonlFiles, this.projectScanTimer, this.activeAgentId, @@ -266,7 +268,7 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { } sendExistingAgents(this.agents, this.context, this.webview); } else if (message.type === 'openSessionsFolder') { - const projectDir = getProjectDirPath(); + const projectDir = getProjectDirPath('claude'); // Use claude as default if (projectDir && fs.existsSync(projectDir)) { vscode.env.openExternal(vscode.Uri.file(projectDir)); } diff --git a/src/agentManager.ts b/src/agentManager.ts index f7c17a9d..9600f740 100644 --- a/src/agentManager.ts +++ b/src/agentManager.ts @@ -12,17 +12,33 @@ import { import { ensureProjectScan, readNewLines, startFileWatching } from './fileWatcher.js'; import { migrateAndLoadLayout } from './layoutPersistence.js'; import { cancelPermissionTimer, cancelWaitingTimer } from './timerManager.js'; -import type { AgentState, PersistedAgent } from './types.js'; +import type { AgentState, AgentType, PersistedAgent } from './types.js'; -export function getProjectDirPath(cwd?: string): string | null { +export function getProjectDirPath(agentType: AgentType | string, cwd?: string): string | null { const workspacePath = cwd || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspacePath) return null; const dirName = workspacePath.replace(/[^a-zA-Z0-9-]/g, '-'); - const projectDir = path.join(os.homedir(), '.claude', 'projects', dirName); - console.log(`[Pixel Agents] Project dir: ${workspacePath} → ${dirName}`); + + let agentFolder = '.claude'; + if (agentType === 'codex') agentFolder = '.codex'; + if (agentType === 'antigravity') agentFolder = '.antigravity'; + + const projectDir = path.join(os.homedir(), agentFolder, 'projects', dirName); + console.log(`[Pixel Agents] Project dir: ${workspacePath} → ${dirName} for ${agentType}`); return projectDir; } +export function getAllProjectDirPaths(cwd?: string): string[] { + const workspacePath = cwd || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspacePath) return []; + const dirName = workspacePath.replace(/[^a-zA-Z0-9-]/g, '-'); + return [ + path.join(os.homedir(), '.claude', 'projects', dirName), + path.join(os.homedir(), '.codex', 'projects', dirName), + path.join(os.homedir(), '.antigravity', 'projects', dirName), + ]; +} + export async function launchNewTerminal( nextAgentIdRef: { current: number }, nextTerminalIndexRef: { current: number }, @@ -37,22 +53,38 @@ export async function launchNewTerminal( projectScanTimerRef: { current: ReturnType | null }, webview: vscode.Webview | undefined, persistAgents: () => void, + agentType: string, folderPath?: string, ): Promise { const folders = vscode.workspace.workspaceFolders; const cwd = folderPath || folders?.[0]?.uri.fsPath; const isMultiRoot = !!(folders && folders.length > 1); const idx = nextTerminalIndexRef.current++; + + // Format the agent name based on type + const agentNamePrefix = agentType.charAt(0).toUpperCase() + agentType.slice(1); + const terminalName = + agentType === 'claude' ? `${TERMINAL_NAME_PREFIX} #${idx}` : `${agentNamePrefix} #${idx}`; + const terminal = vscode.window.createTerminal({ - name: `${TERMINAL_NAME_PREFIX} #${idx}`, + name: terminalName, cwd, }); terminal.show(); const sessionId = crypto.randomUUID(); - terminal.sendText(`claude --session-id ${sessionId}`); - const projectDir = getProjectDirPath(cwd); + if (agentType === 'claude') { + terminal.sendText(`claude --session-id ${sessionId}`); + } else if (agentType === 'codex') { + terminal.sendText(`codex --session-id ${sessionId}`); + } else if (agentType === 'antigravity') { + terminal.sendText(`antigravity --session-id ${sessionId}`); + } else { + terminal.sendText(`${agentType} --session-id ${sessionId}`); // Fallback + } + + const projectDir = getProjectDirPath(agentType, cwd); if (!projectDir) { console.log(`[Pixel Agents] No project dir, cannot track agent`); return; @@ -70,6 +102,7 @@ export async function launchNewTerminal( terminalRef: terminal, projectDir, jsonlFile: expectedFile, + agentType: agentType as AgentType, fileOffset: 0, lineBuffer: '', activeToolIds: new Set(), @@ -90,7 +123,7 @@ export async function launchNewTerminal( webview?.postMessage({ type: 'agentCreated', id, folderName }); ensureProjectScan( - projectDir, + [projectDir], knownJsonlFiles, projectScanTimerRef, activeAgentIdRef, @@ -186,6 +219,7 @@ export function persistAgents( terminalName: agent.terminalRef.name, jsonlFile: agent.jsonlFile, projectDir: agent.projectDir, + agentType: agent.agentType, folderName: agent.folderName, }); } @@ -225,6 +259,7 @@ export function restoreAgents( terminalRef: terminal, projectDir: p.projectDir, jsonlFile: p.jsonlFile, + agentType: p.agentType || 'claude', // Fallback to claude for older versions fileOffset: 0, lineBuffer: '', activeToolIds: new Set(), @@ -313,7 +348,7 @@ export function restoreAgents( // Start project scan for /clear detection if (restoredProjectDir) { ensureProjectScan( - restoredProjectDir, + [restoredProjectDir], knownJsonlFiles, projectScanTimerRef, activeAgentIdRef, diff --git a/src/fileWatcher.ts b/src/fileWatcher.ts index f2a60dfd..8001c0ed 100644 --- a/src/fileWatcher.ts +++ b/src/fileWatcher.ts @@ -96,7 +96,7 @@ export function readNewLines( } export function ensureProjectScan( - projectDir: string, + projectDirs: string[], knownJsonlFiles: Set, projectScanTimerRef: { current: ReturnType | null }, activeAgentIdRef: { current: number | null }, @@ -111,32 +111,36 @@ export function ensureProjectScan( ): void { if (projectScanTimerRef.current) return; // Seed with all existing JSONL files so we only react to truly new ones - try { - const files = fs - .readdirSync(projectDir) - .filter((f) => f.endsWith('.jsonl')) - .map((f) => path.join(projectDir, f)); - for (const f of files) { - knownJsonlFiles.add(f); + for (const projectDir of projectDirs) { + try { + const files = fs + .readdirSync(projectDir) + .filter((f) => f.endsWith('.jsonl')) + .map((f) => path.join(projectDir, f)); + for (const f of files) { + knownJsonlFiles.add(f); + } + } catch { + /* dir may not exist yet */ } - } catch { - /* dir may not exist yet */ } projectScanTimerRef.current = setInterval(() => { - scanForNewJsonlFiles( - projectDir, - knownJsonlFiles, - activeAgentIdRef, - nextAgentIdRef, - agents, - fileWatchers, - pollingTimers, - waitingTimers, - permissionTimers, - webview, - persistAgents, - ); + for (const projectDir of projectDirs) { + scanForNewJsonlFiles( + projectDir, + knownJsonlFiles, + activeAgentIdRef, + nextAgentIdRef, + agents, + fileWatchers, + pollingTimers, + waitingTimers, + permissionTimers, + webview, + persistAgents, + ); + } }, PROJECT_SCAN_INTERVAL_MS); } @@ -235,6 +239,7 @@ function adoptTerminalForFile( terminalRef: terminal, projectDir, jsonlFile, + agentType: 'claude', // Default for adopted terminals fileOffset: 0, lineBuffer: '', activeToolIds: new Set(), diff --git a/src/types.ts b/src/types.ts index feeec137..68a035d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,13 @@ import type * as vscode from 'vscode'; +export type AgentType = 'claude' | 'codex' | 'antigravity'; + export interface AgentState { id: number; terminalRef: vscode.Terminal; projectDir: string; jsonlFile: string; + agentType: AgentType; fileOffset: number; lineBuffer: string; activeToolIds: Set; @@ -24,6 +27,7 @@ export interface PersistedAgent { terminalName: string; jsonlFile: string; projectDir: string; + agentType?: AgentType; /** Workspace folder name (only set for multi-root workspaces) */ folderName?: string; } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index ed75ef18..b6301b32 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -266,7 +266,7 @@ function App() { void; + onOpenAgent: (agentType: string) => void; onToggleEditMode: () => void; isDebugMode: boolean; onToggleDebugMode: () => void; @@ -46,7 +46,7 @@ const btnActive: React.CSSProperties = { export function BottomToolbar({ isEditMode, - onOpenClaude, + onOpenAgent, onToggleEditMode, isDebugMode, onToggleDebugMode, @@ -54,40 +54,67 @@ export function BottomToolbar({ }: BottomToolbarProps) { const [hovered, setHovered] = useState(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isAgentMenuOpen, setIsAgentMenuOpen] = useState(false); const [isFolderPickerOpen, setIsFolderPickerOpen] = useState(false); const [hoveredFolder, setHoveredFolder] = useState(null); - const folderPickerRef = useRef(null); + const [hoveredAgent, setHoveredAgent] = useState(null); + const [pendingAgentType, setPendingAgentType] = useState(null); + const menuRef = useRef(null); - // Close folder picker on outside click + // Close menus on outside click useEffect(() => { - if (!isFolderPickerOpen) return; + if (!isAgentMenuOpen && !isFolderPickerOpen) return; const handleClick = (e: MouseEvent) => { - if (folderPickerRef.current && !folderPickerRef.current.contains(e.target as Node)) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setIsAgentMenuOpen(false); setIsFolderPickerOpen(false); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); - }, [isFolderPickerOpen]); + }, [isAgentMenuOpen, isFolderPickerOpen]); const hasMultipleFolders = workspaceFolders.length > 1; const handleAgentClick = () => { + setIsAgentMenuOpen((v) => !v); + setIsFolderPickerOpen(false); + }; + + const handleAgentSelect = (type: string) => { if (hasMultipleFolders) { - setIsFolderPickerOpen((v) => !v); + setPendingAgentType(type); + setIsAgentMenuOpen(false); + setIsFolderPickerOpen(true); } else { - onOpenClaude(); + onOpenAgent(type); + setIsAgentMenuOpen(false); } }; const handleFolderSelect = (folder: WorkspaceFolder) => { setIsFolderPickerOpen(false); - vscode.postMessage({ type: 'openClaude', folderPath: folder.path }); + if (pendingAgentType) { + vscode.postMessage({ + type: 'openAgent', + agentType: pendingAgentType, + folderPath: folder.path, + }); + setPendingAgentType(null); + } else { + vscode.postMessage({ type: 'openAgent', agentType: 'claude', folderPath: folder.path }); + } }; + const agentTypes = [ + { id: 'claude', label: 'Claude' }, + { id: 'codex', label: 'Codex' }, + { id: 'antigravity', label: 'Antigravity' }, + ]; + return (
-
+
+ {isAgentMenuOpen && ( +
+ {agentTypes.map((agent) => ( + + ))} +
+ )} {isFolderPickerOpen && (
; saveTimerRef: React.MutableRefObject | null>; setLastSavedLayout: (layout: OfficeLayout) => void; - handleOpenClaude: () => void; + handleOpenAgent: (agentType: string) => void; handleToggleEditMode: () => void; handleToolChange: (tool: EditToolType) => void; handleTileTypeChange: (type: TileTypeVal) => void; @@ -103,8 +103,8 @@ export function useEditorActions( [getOfficeState, editorState, saveLayout], ); - const handleOpenClaude = useCallback(() => { - vscode.postMessage({ type: 'openClaude' }); + const handleOpenAgent = useCallback((agentType: string) => { + vscode.postMessage({ type: 'openAgent', agentType }); }, []); const handleToggleEditMode = useCallback(() => { @@ -611,7 +611,7 @@ export function useEditorActions( panRef, saveTimerRef, setLastSavedLayout, - handleOpenClaude, + handleOpenAgent, handleToggleEditMode, handleToolChange, handleTileTypeChange,