mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat: add custom GitHub CLI path setting for Auto Run worktree features
- Add ghPath setting to specify custom path to gh binary (e.g., /opt/homebrew/bin/gh) - Update git:checkGhCli and git:createPR IPC handlers to accept optional ghPath - Add UI in Settings > General for configuring the gh path - Pass ghPath through BatchRunnerModal and useBatchProcessor for PR creation - Include test infrastructure setup (vitest) and misc updates Claude ID: 295a322c-974c-4b49-b31d-f7be18819332 Maestro ID: b9bc0d08-5be2-4fdf-93cd-5618a8d53b35
This commit is contained in:
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -75,6 +75,21 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using version: $VERSION"
|
||||
|
||||
# Update package.json with the release version
|
||||
# This ensures CLI and other components read the correct version
|
||||
- name: Update package.json version
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.VERSION }}
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = process.env.VERSION;
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
echo "Updated package.json to version $VERSION"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Tests
|
||||
do-wishlist.sh
|
||||
do-housekeeping.sh
|
||||
coverage/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
1849
package-lock.json
generated
1849
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maestro",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Multi-Instance AI Coding Console - Unified IDE for managing multiple AI coding assistants",
|
||||
"main": "dist/main/index.js",
|
||||
"author": {
|
||||
@@ -33,7 +33,10 @@
|
||||
"package:linux": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --linux",
|
||||
"start": "electron .",
|
||||
"clean": "rm -rf dist release node_modules/.vite",
|
||||
"postinstall": "electron-rebuild -f -w node-pty"
|
||||
"postinstall": "electron-rebuild -f -w node-pty",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.maestro.app",
|
||||
@@ -146,6 +149,8 @@
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
@@ -156,6 +161,7 @@
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"canvas": "^3.2.0",
|
||||
"concurrently": "^8.2.2",
|
||||
@@ -163,6 +169,7 @@
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "^0.24.2",
|
||||
"jsdom": "^27.2.0",
|
||||
"lucide-react": "^0.303.0",
|
||||
"postcss": "^8.4.33",
|
||||
"react": "^18.2.0",
|
||||
@@ -170,6 +177,7 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-electron": "^0.28.2"
|
||||
"vite-plugin-electron": "^0.28.2",
|
||||
"vitest": "^4.0.15"
|
||||
}
|
||||
}
|
||||
|
||||
92
src/__tests__/setup.ts
Normal file
92
src/__tests__/setup.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock window.matchMedia for components that use media queries
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock IntersectionObserver
|
||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.maestro API (Electron IPC bridge)
|
||||
const mockMaestro = {
|
||||
settings: {
|
||||
get: vi.fn().mockResolvedValue(undefined),
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
getAll: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
sessions: {
|
||||
get: vi.fn().mockResolvedValue([]),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
groups: {
|
||||
get: vi.fn().mockResolvedValue([]),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
process: {
|
||||
spawn: vi.fn().mockResolvedValue({ pid: 12345 }),
|
||||
write: vi.fn().mockResolvedValue(undefined),
|
||||
kill: vi.fn().mockResolvedValue(undefined),
|
||||
resize: vi.fn().mockResolvedValue(undefined),
|
||||
onOutput: vi.fn().mockReturnValue(() => {}),
|
||||
onExit: vi.fn().mockReturnValue(() => {}),
|
||||
},
|
||||
git: {
|
||||
status: vi.fn().mockResolvedValue({ files: [], branch: 'main' }),
|
||||
diff: vi.fn().mockResolvedValue(''),
|
||||
isRepo: vi.fn().mockResolvedValue(true),
|
||||
numstat: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
fs: {
|
||||
readDir: vi.fn().mockResolvedValue([]),
|
||||
readFile: vi.fn().mockResolvedValue(''),
|
||||
},
|
||||
agents: {
|
||||
detect: vi.fn().mockResolvedValue([]),
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
config: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
claude: {
|
||||
listSessions: vi.fn().mockResolvedValue([]),
|
||||
readSession: vi.fn().mockResolvedValue(null),
|
||||
searchSessions: vi.fn().mockResolvedValue([]),
|
||||
getGlobalStats: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
logger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
dialog: {
|
||||
selectFolder: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
shells: {
|
||||
detect: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'maestro', {
|
||||
writable: true,
|
||||
value: mockMaestro,
|
||||
});
|
||||
@@ -3,6 +3,8 @@
|
||||
// Command-line interface for Maestro
|
||||
|
||||
import { Command } from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { listGroups } from './commands/list-groups';
|
||||
import { listAgents } from './commands/list-agents';
|
||||
import { listPlaybooks } from './commands/list-playbooks';
|
||||
@@ -10,12 +12,24 @@ import { showPlaybook } from './commands/show-playbook';
|
||||
import { showAgent } from './commands/show-agent';
|
||||
import { runPlaybook } from './commands/run-playbook';
|
||||
|
||||
// Read version from package.json at runtime
|
||||
function getVersion(): string {
|
||||
try {
|
||||
// When bundled, __dirname points to dist/cli, so go up to project root
|
||||
const packagePath = path.resolve(__dirname, '../../package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('maestro-cli')
|
||||
.description('Command-line interface for Maestro')
|
||||
.version('0.1.0');
|
||||
.version(getVersion());
|
||||
|
||||
// List commands
|
||||
const list = program.command('list').description('List resources');
|
||||
|
||||
@@ -1472,8 +1472,12 @@ function setupIpcHandlers() {
|
||||
});
|
||||
|
||||
// Create a PR from the worktree branch to a base branch
|
||||
ipcMain.handle('git:createPR', async (_, worktreePath: string, baseBranch: string, title: string, body: string) => {
|
||||
// ghPath parameter allows specifying custom path to gh binary
|
||||
ipcMain.handle('git:createPR', async (_, worktreePath: string, baseBranch: string, title: string, body: string, ghPath?: string) => {
|
||||
try {
|
||||
// Use custom path if provided, otherwise fall back to 'gh' (expects it in PATH)
|
||||
const ghCommand = ghPath || 'gh';
|
||||
|
||||
// First, push the current branch to origin
|
||||
const pushResult = await execFileNoThrow('git', ['push', '-u', 'origin', 'HEAD'], worktreePath);
|
||||
if (pushResult.exitCode !== 0) {
|
||||
@@ -1481,7 +1485,7 @@ function setupIpcHandlers() {
|
||||
}
|
||||
|
||||
// Create the PR using gh CLI
|
||||
const prResult = await execFileNoThrow('gh', [
|
||||
const prResult = await execFileNoThrow(ghCommand, [
|
||||
'pr', 'create',
|
||||
'--base', baseBranch,
|
||||
'--title', title,
|
||||
@@ -1505,16 +1509,20 @@ function setupIpcHandlers() {
|
||||
});
|
||||
|
||||
// Check if GitHub CLI (gh) is installed and authenticated
|
||||
ipcMain.handle('git:checkGhCli', async () => {
|
||||
// ghPath parameter allows specifying custom path to gh binary (e.g., /opt/homebrew/bin/gh)
|
||||
ipcMain.handle('git:checkGhCli', async (_, ghPath?: string) => {
|
||||
try {
|
||||
// Use custom path if provided, otherwise fall back to 'gh' (expects it in PATH)
|
||||
const ghCommand = ghPath || 'gh';
|
||||
|
||||
// Check if gh is installed by running gh --version
|
||||
const versionResult = await execFileNoThrow('gh', ['--version']);
|
||||
const versionResult = await execFileNoThrow(ghCommand, ['--version']);
|
||||
if (versionResult.exitCode !== 0) {
|
||||
return { installed: false, authenticated: false };
|
||||
}
|
||||
|
||||
// Check if gh is authenticated by running gh auth status
|
||||
const authResult = await execFileNoThrow('gh', ['auth', 'status']);
|
||||
const authResult = await execFileNoThrow(ghCommand, ['auth', 'status']);
|
||||
const authenticated = authResult.exitCode === 0;
|
||||
|
||||
return { installed: true, authenticated };
|
||||
|
||||
@@ -255,8 +255,8 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
hasUncommittedChanges: boolean;
|
||||
error?: string;
|
||||
}>,
|
||||
createPR: (worktreePath: string, baseBranch: string, title: string, body: string) =>
|
||||
ipcRenderer.invoke('git:createPR', worktreePath, baseBranch, title, body) as Promise<{
|
||||
createPR: (worktreePath: string, baseBranch: string, title: string, body: string, ghPath?: string) =>
|
||||
ipcRenderer.invoke('git:createPR', worktreePath, baseBranch, title, body, ghPath) as Promise<{
|
||||
success: boolean;
|
||||
prUrl?: string;
|
||||
error?: string;
|
||||
@@ -267,8 +267,8 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
branch?: string;
|
||||
error?: string;
|
||||
}>,
|
||||
checkGhCli: () =>
|
||||
ipcRenderer.invoke('git:checkGhCli') as Promise<{
|
||||
checkGhCli: (ghPath?: string) =>
|
||||
ipcRenderer.invoke('git:checkGhCli', ghPath) as Promise<{
|
||||
installed: boolean;
|
||||
authenticated: boolean;
|
||||
}>,
|
||||
@@ -701,7 +701,7 @@ export interface MaestroAPI {
|
||||
hasUncommittedChanges: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
createPR: (worktreePath: string, baseBranch: string, title: string, body: string) => Promise<{
|
||||
createPR: (worktreePath: string, baseBranch: string, title: string, body: string, ghPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
prUrl?: string;
|
||||
error?: string;
|
||||
@@ -711,7 +711,7 @@ export interface MaestroAPI {
|
||||
branch?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
checkGhCli: () => Promise<{
|
||||
checkGhCli: (ghPath?: string) => Promise<{
|
||||
installed: boolean;
|
||||
authenticated: boolean;
|
||||
}>;
|
||||
|
||||
@@ -130,11 +130,13 @@ export default function MaestroConsole() {
|
||||
apiKey, setApiKey,
|
||||
defaultAgent, setDefaultAgent,
|
||||
defaultShell, setDefaultShell,
|
||||
ghPath, setGhPath,
|
||||
fontFamily, setFontFamily,
|
||||
fontSize, setFontSize,
|
||||
activeThemeId, setActiveThemeId,
|
||||
enterToSendAI, setEnterToSendAI,
|
||||
enterToSendTerminal, setEnterToSendTerminal,
|
||||
defaultSaveToHistory, setDefaultSaveToHistory,
|
||||
leftSidebarWidth, setLeftSidebarWidth,
|
||||
rightPanelWidth, setRightPanelWidth,
|
||||
markdownRawMode, setMarkdownRawMode,
|
||||
@@ -1703,7 +1705,7 @@ export default function MaestroConsole() {
|
||||
if (s.id !== sessionId) return s;
|
||||
|
||||
// Use createTab helper
|
||||
const result = createTab(s);
|
||||
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
|
||||
newTabId = result.tab.id;
|
||||
return result.session;
|
||||
}));
|
||||
@@ -1840,7 +1842,7 @@ export default function MaestroConsole() {
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
// Combine built-in slash commands with custom AI commands for autocomplete
|
||||
// Combine built-in slash commands with custom AI commands AND Claude Code commands for autocomplete
|
||||
// Filter out isSystemCommand entries since those are already in slashCommands with execute functions
|
||||
const allSlashCommands = useMemo(() => {
|
||||
const customCommandsAsSlash = customAICommands
|
||||
@@ -1851,8 +1853,14 @@ export default function MaestroConsole() {
|
||||
aiOnly: true, // Custom AI commands are only available in AI mode
|
||||
prompt: cmd.prompt, // Include prompt for execution
|
||||
}));
|
||||
return [...slashCommands, ...customCommandsAsSlash];
|
||||
}, [customAICommands]);
|
||||
// Include Claude Code commands from the active session
|
||||
const claudeCommands = (activeSession?.claudeCommands || []).map(cmd => ({
|
||||
command: cmd.command,
|
||||
description: cmd.description,
|
||||
aiOnly: true, // Claude commands are only available in AI mode
|
||||
}));
|
||||
return [...slashCommands, ...customCommandsAsSlash, ...claudeCommands];
|
||||
}, [customAICommands, activeSession?.claudeCommands]);
|
||||
|
||||
// Derive current input value and setter based on active session mode
|
||||
// For AI mode: use active tab's inputValue (stored per-tab)
|
||||
@@ -2775,7 +2783,8 @@ export default function MaestroConsole() {
|
||||
claudeSessionId,
|
||||
logs: messages,
|
||||
name,
|
||||
starred: isStarred
|
||||
starred: isStarred,
|
||||
saveToHistory: defaultSaveToHistory
|
||||
});
|
||||
|
||||
console.log('[handleResumeSession] Created tab:', newTab.id, 'with', newTab.logs.length, 'logs, activeTabId:', updatedSession.activeTabId);
|
||||
@@ -3432,7 +3441,7 @@ export default function MaestroConsole() {
|
||||
}
|
||||
if (ctx.isTabShortcut(e, 'newTab')) {
|
||||
e.preventDefault();
|
||||
const result = ctx.createTab(ctx.activeSession);
|
||||
const result = ctx.createTab(ctx.activeSession, { saveToHistory: ctx.defaultSaveToHistory });
|
||||
ctx.setSessions(prev => prev.map(s =>
|
||||
s.id === ctx.activeSession!.id ? result.session : s
|
||||
));
|
||||
@@ -3838,7 +3847,8 @@ export default function MaestroConsole() {
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
createdAt: Date.now(),
|
||||
state: 'idle'
|
||||
state: 'idle',
|
||||
saveToHistory: defaultSaveToHistory
|
||||
};
|
||||
|
||||
const newSession: Session = {
|
||||
@@ -4039,7 +4049,7 @@ export default function MaestroConsole() {
|
||||
processMonitorOpen, logViewerOpen, createGroupModalOpen, confirmModalOpen, renameInstanceModalOpen,
|
||||
renameGroupModalOpen, activeSession, previewFile, fileTreeFilter, fileTreeFilterOpen, gitDiffPreview,
|
||||
gitLogOpen, lightboxImage, hasOpenLayers, hasOpenModal, visibleSessions, sortedSessions, groups,
|
||||
bookmarksCollapsed, leftSidebarOpen, editingSessionId, editingGroupId, markdownRawMode,
|
||||
bookmarksCollapsed, leftSidebarOpen, editingSessionId, editingGroupId, markdownRawMode, defaultSaveToHistory,
|
||||
setLeftSidebarOpen, setRightPanelOpen, addNewSession, deleteSession, setQuickActionInitialMode,
|
||||
setQuickActionOpen, cycleSession, toggleInputMode, setShortcutsHelpOpen, setSettingsModalOpen,
|
||||
setSettingsTab, setActiveRightTab, handleSetActiveRightTab, setActiveFocus, setBookmarksCollapsed, setGroups,
|
||||
@@ -4231,6 +4241,7 @@ export default function MaestroConsole() {
|
||||
if (result.success) {
|
||||
const newFiles = result.files || [];
|
||||
setAutoRunDocumentList(newFiles);
|
||||
setAutoRunDocumentTree((result.tree as Array<{ name: string; type: 'file' | 'folder'; path: string; children?: unknown[] }>) || []);
|
||||
setAutoRunIsLoadingDocuments(false);
|
||||
|
||||
// Show flash notification with result
|
||||
@@ -6329,7 +6340,7 @@ export default function MaestroConsole() {
|
||||
if (activeSession) {
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSession.id) return s;
|
||||
const result = createTab(s);
|
||||
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
|
||||
return result.session;
|
||||
}));
|
||||
setActiveClaudeSessionId(null);
|
||||
@@ -6526,7 +6537,7 @@ export default function MaestroConsole() {
|
||||
// Use functional setState to compute from fresh state (avoids stale closure issues)
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSession.id) return s;
|
||||
const result = createTab(s);
|
||||
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
|
||||
return result.session;
|
||||
}));
|
||||
}}
|
||||
@@ -6781,6 +6792,7 @@ export default function MaestroConsole() {
|
||||
onRefreshDocuments={handleAutoRunRefresh}
|
||||
sessionId={activeSession.id}
|
||||
sessionCwd={activeSession.cwd}
|
||||
ghPath={ghPath}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6877,10 +6889,14 @@ export default function MaestroConsole() {
|
||||
setDefaultAgent={setDefaultAgent}
|
||||
defaultShell={defaultShell}
|
||||
setDefaultShell={setDefaultShell}
|
||||
ghPath={ghPath}
|
||||
setGhPath={setGhPath}
|
||||
enterToSendAI={enterToSendAI}
|
||||
setEnterToSendAI={setEnterToSendAI}
|
||||
enterToSendTerminal={enterToSendTerminal}
|
||||
setEnterToSendTerminal={setEnterToSendTerminal}
|
||||
defaultSaveToHistory={defaultSaveToHistory}
|
||||
setDefaultSaveToHistory={setDefaultSaveToHistory}
|
||||
fontFamily={fontFamily}
|
||||
setFontFamily={setFontFamily}
|
||||
fontSize={fontSize}
|
||||
|
||||
@@ -87,6 +87,8 @@ interface BatchRunnerModalProps {
|
||||
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
|
||||
@@ -129,7 +131,8 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
|
||||
getDocumentTaskCount,
|
||||
onRefreshDocuments,
|
||||
sessionId,
|
||||
sessionCwd
|
||||
sessionCwd,
|
||||
ghPath
|
||||
} = props;
|
||||
|
||||
// Document list state
|
||||
@@ -270,7 +273,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
|
||||
if (isRepo) {
|
||||
const [branchResult, ghResult] = await Promise.all([
|
||||
window.maestro.git.branches(sessionCwd),
|
||||
window.maestro.git.checkGhCli()
|
||||
window.maestro.git.checkGhCli(ghPath || undefined)
|
||||
]);
|
||||
|
||||
if (branchResult.branches && branchResult.branches.length > 0) {
|
||||
@@ -295,7 +298,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
|
||||
};
|
||||
|
||||
checkGitRepo();
|
||||
}, [sessionCwd]);
|
||||
}, [sessionCwd, ghPath]);
|
||||
|
||||
// Validate worktree path when it changes (debounced 500ms)
|
||||
useEffect(() => {
|
||||
@@ -544,7 +547,8 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
|
||||
path: worktreePath,
|
||||
branchName,
|
||||
createPROnCompletion,
|
||||
prTargetBranch
|
||||
prTargetBranch,
|
||||
ghPath: ghPath || undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -265,15 +265,16 @@ export function GitStatusWidget({ cwd, isGitRepo, theme, onViewDiff }: GitStatus
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="text-[10px] p-2 text-center border-t"
|
||||
<button
|
||||
onClick={onViewDiff}
|
||||
className="text-[10px] p-2 text-center border-t w-full hover:bg-white/5 transition-colors cursor-pointer"
|
||||
style={{
|
||||
color: theme.colors.textDim,
|
||||
borderColor: theme.colors.border
|
||||
}}
|
||||
>
|
||||
Click to view full diff
|
||||
</div>
|
||||
View Full Diff
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -123,21 +123,11 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
? (shellHistory.length > 0 ? shellHistory : legacyHistory)
|
||||
: (aiHistory.length > 0 ? aiHistory : legacyHistory);
|
||||
|
||||
// Combine built-in slash commands with Claude-specific commands (for AI mode only)
|
||||
// Memoize to avoid recreating arrays on every render
|
||||
const allSlashCommands = useMemo(() => {
|
||||
const claudeCommands: SlashCommand[] = (session.claudeCommands || []).map(cmd => ({
|
||||
command: cmd.command,
|
||||
description: cmd.description,
|
||||
aiOnly: true, // Claude commands are only available in AI mode
|
||||
}));
|
||||
return [...slashCommands, ...claudeCommands];
|
||||
}, [session.claudeCommands, slashCommands]);
|
||||
|
||||
// Use the slash commands passed from App.tsx (already includes custom + Claude commands)
|
||||
// Memoize filtered slash commands to avoid filtering on every render
|
||||
const inputValueLower = inputValue.toLowerCase();
|
||||
const filteredSlashCommands = useMemo(() => {
|
||||
return allSlashCommands.filter(cmd => {
|
||||
return slashCommands.filter(cmd => {
|
||||
// Check if command is only available in terminal mode
|
||||
if (cmd.terminalOnly && !isTerminalMode) return false;
|
||||
// Check if command is only available in AI mode
|
||||
@@ -145,7 +135,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
// Check if command matches input
|
||||
return cmd.command.toLowerCase().startsWith(inputValueLower);
|
||||
});
|
||||
}, [allSlashCommands, isTerminalMode, inputValueLower]);
|
||||
}, [slashCommands, isTerminalMode, inputValueLower]);
|
||||
|
||||
// Ensure selectedSlashCommandIndex is valid for the filtered list
|
||||
const safeSelectedIndex = Math.min(
|
||||
@@ -184,7 +174,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [safeSelectedIndex, slashCommandOpen]);
|
||||
}, [safeSelectedIndex, slashCommandOpen, selectedSlashCommandIndex]);
|
||||
|
||||
// Scroll selected tab completion item into view when index changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Volume2, Square, Cpu, Clock, Settings, Palette, Sparkles } from 'lucide-react';
|
||||
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Volume2, Square, Cpu, Clock, Settings, Palette, Sparkles, History } from 'lucide-react';
|
||||
import type { AgentConfig, Theme, Shortcut, ShellInfo, CustomAICommand } from '../types';
|
||||
import { useLayerStack } from '../contexts/LayerStackContext';
|
||||
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
||||
@@ -42,10 +42,14 @@ interface SettingsModalProps {
|
||||
setMaxOutputLines: (lines: number) => void;
|
||||
defaultShell: string;
|
||||
setDefaultShell: (shell: string) => void;
|
||||
ghPath: string;
|
||||
setGhPath: (path: string) => void;
|
||||
enterToSendAI: boolean;
|
||||
setEnterToSendAI: (value: boolean) => void;
|
||||
enterToSendTerminal: boolean;
|
||||
setEnterToSendTerminal: (value: boolean) => void;
|
||||
defaultSaveToHistory: boolean;
|
||||
setDefaultSaveToHistory: (value: boolean) => void;
|
||||
osNotificationsEnabled: boolean;
|
||||
setOsNotificationsEnabled: (value: boolean) => void;
|
||||
audioFeedbackEnabled: boolean;
|
||||
@@ -1166,6 +1170,39 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GitHub CLI Path */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
|
||||
<Terminal className="w-3 h-3" />
|
||||
GitHub CLI (gh) Path
|
||||
</label>
|
||||
<div className="p-3 rounded border" style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}>
|
||||
<label className="block text-xs opacity-60 mb-1">Custom Path (optional)</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={props.ghPath}
|
||||
onChange={(e) => props.setGhPath(e.target.value)}
|
||||
placeholder="/opt/homebrew/bin/gh"
|
||||
className="flex-1 p-1.5 rounded border bg-transparent outline-none text-xs font-mono"
|
||||
style={{ borderColor: theme.colors.border, color: theme.colors.textMain }}
|
||||
/>
|
||||
{props.ghPath && (
|
||||
<button
|
||||
onClick={() => props.setGhPath('')}
|
||||
className="px-2 py-1 rounded text-xs"
|
||||
style={{ backgroundColor: theme.colors.bgActivity, color: theme.colors.textDim }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs opacity-40 mt-2">
|
||||
Specify the full path to the <code className="px-1 py-0.5 rounded" style={{ backgroundColor: theme.colors.bgActivity }}>gh</code> binary if it's not in your PATH. Used for Auto Run worktree features.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Behavior Settings */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
|
||||
@@ -1222,6 +1259,34 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default History Toggle */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
|
||||
<History className="w-3 h-3" />
|
||||
Default History Toggle
|
||||
</label>
|
||||
<label
|
||||
className="flex items-center gap-3 p-3 rounded border cursor-pointer hover:bg-opacity-10"
|
||||
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.defaultSaveToHistory}
|
||||
onChange={(e) => props.setDefaultSaveToHistory(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
style={{ accentColor: theme.colors.accent }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium" style={{ color: theme.colors.textMain }}>
|
||||
Enable "History" by default for new tabs
|
||||
</div>
|
||||
<div className="text-xs opacity-50 mt-0.5" style={{ color: theme.colors.textDim }}>
|
||||
When enabled, new AI tabs will have the "History" toggle on by default, saving a synopsis after each completion
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -901,12 +901,13 @@ ${docList}
|
||||
const prTitle = `Auto Run: ${documents.length} document(s) processed`;
|
||||
const prBody = generatePRBody(documents, totalCompletedTasks);
|
||||
|
||||
// Create the PR
|
||||
// Create the PR (pass ghPath if configured)
|
||||
const prResult = await window.maestro.git.createPR(
|
||||
effectiveCwd,
|
||||
baseBranch,
|
||||
prTitle,
|
||||
prBody
|
||||
prBody,
|
||||
worktree.ghPath
|
||||
);
|
||||
|
||||
if (prResult.success) {
|
||||
|
||||
@@ -69,6 +69,10 @@ export interface UseSettingsReturn {
|
||||
defaultShell: string;
|
||||
setDefaultShell: (value: string) => void;
|
||||
|
||||
// GitHub CLI settings
|
||||
ghPath: string;
|
||||
setGhPath: (value: string) => void;
|
||||
|
||||
// Font settings
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
@@ -84,6 +88,8 @@ export interface UseSettingsReturn {
|
||||
setEnterToSendAI: (value: boolean) => void;
|
||||
enterToSendTerminal: boolean;
|
||||
setEnterToSendTerminal: (value: boolean) => void;
|
||||
defaultSaveToHistory: boolean;
|
||||
setDefaultSaveToHistory: (value: boolean) => void;
|
||||
leftSidebarWidth: number;
|
||||
rightPanelWidth: number;
|
||||
markdownRawMode: boolean;
|
||||
@@ -155,6 +161,9 @@ export function useSettings(): UseSettingsReturn {
|
||||
// Shell Config
|
||||
const [defaultShell, setDefaultShellState] = useState('zsh');
|
||||
|
||||
// GitHub CLI Config
|
||||
const [ghPath, setGhPathState] = useState('');
|
||||
|
||||
// Font Config
|
||||
const [fontFamily, setFontFamilyState] = useState('Roboto Mono, Menlo, "Courier New", monospace');
|
||||
const [fontSize, setFontSizeState] = useState(14);
|
||||
@@ -164,6 +173,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
const [activeThemeId, setActiveThemeIdState] = useState<ThemeId>('dracula');
|
||||
const [enterToSendAI, setEnterToSendAIState] = useState(false); // AI mode defaults to Command+Enter
|
||||
const [enterToSendTerminal, setEnterToSendTerminalState] = useState(true); // Terminal defaults to Enter
|
||||
const [defaultSaveToHistory, setDefaultSaveToHistoryState] = useState(false); // History toggle defaults to off
|
||||
const [leftSidebarWidth, setLeftSidebarWidthState] = useState(256);
|
||||
const [rightPanelWidth, setRightPanelWidthState] = useState(384);
|
||||
const [markdownRawMode, setMarkdownRawModeState] = useState(false);
|
||||
@@ -225,6 +235,11 @@ export function useSettings(): UseSettingsReturn {
|
||||
window.maestro.settings.set('defaultShell', value);
|
||||
};
|
||||
|
||||
const setGhPath = (value: string) => {
|
||||
setGhPathState(value);
|
||||
window.maestro.settings.set('ghPath', value);
|
||||
};
|
||||
|
||||
const setFontFamily = (value: string) => {
|
||||
setFontFamilyState(value);
|
||||
window.maestro.settings.set('fontFamily', value);
|
||||
@@ -255,6 +270,11 @@ export function useSettings(): UseSettingsReturn {
|
||||
window.maestro.settings.set('enterToSendTerminal', value);
|
||||
};
|
||||
|
||||
const setDefaultSaveToHistory = (value: boolean) => {
|
||||
setDefaultSaveToHistoryState(value);
|
||||
window.maestro.settings.set('defaultSaveToHistory', value);
|
||||
};
|
||||
|
||||
const setLeftSidebarWidth = (width: number) => {
|
||||
setLeftSidebarWidthState(width);
|
||||
window.maestro.settings.set('leftSidebarWidth', width);
|
||||
@@ -458,12 +478,14 @@ export function useSettings(): UseSettingsReturn {
|
||||
const loadSettings = async () => {
|
||||
const savedEnterToSendAI = await window.maestro.settings.get('enterToSendAI');
|
||||
const savedEnterToSendTerminal = await window.maestro.settings.get('enterToSendTerminal');
|
||||
const savedDefaultSaveToHistory = await window.maestro.settings.get('defaultSaveToHistory');
|
||||
|
||||
const savedLlmProvider = await window.maestro.settings.get('llmProvider');
|
||||
const savedModelSlug = await window.maestro.settings.get('modelSlug');
|
||||
const savedApiKey = await window.maestro.settings.get('apiKey');
|
||||
const savedDefaultAgent = await window.maestro.settings.get('defaultAgent');
|
||||
const savedDefaultShell = await window.maestro.settings.get('defaultShell');
|
||||
const savedGhPath = await window.maestro.settings.get('ghPath');
|
||||
const savedFontSize = await window.maestro.settings.get('fontSize');
|
||||
const savedFontFamily = await window.maestro.settings.get('fontFamily');
|
||||
const savedCustomFonts = await window.maestro.settings.get('customFonts');
|
||||
@@ -487,12 +509,14 @@ export function useSettings(): UseSettingsReturn {
|
||||
|
||||
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI);
|
||||
if (savedEnterToSendTerminal !== undefined) setEnterToSendTerminalState(savedEnterToSendTerminal);
|
||||
if (savedDefaultSaveToHistory !== undefined) setDefaultSaveToHistoryState(savedDefaultSaveToHistory);
|
||||
|
||||
if (savedLlmProvider !== undefined) setLlmProviderState(savedLlmProvider);
|
||||
if (savedModelSlug !== undefined) setModelSlugState(savedModelSlug);
|
||||
if (savedApiKey !== undefined) setApiKeyState(savedApiKey);
|
||||
if (savedDefaultAgent !== undefined) setDefaultAgentState(savedDefaultAgent);
|
||||
if (savedDefaultShell !== undefined) setDefaultShellState(savedDefaultShell);
|
||||
if (savedGhPath !== undefined) setGhPathState(savedGhPath);
|
||||
if (savedFontSize !== undefined) setFontSizeState(savedFontSize);
|
||||
if (savedFontFamily !== undefined) setFontFamilyState(savedFontFamily);
|
||||
if (savedCustomFonts !== undefined) setCustomFontsState(savedCustomFonts);
|
||||
@@ -565,6 +589,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setDefaultAgent,
|
||||
defaultShell,
|
||||
setDefaultShell,
|
||||
ghPath,
|
||||
setGhPath,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
customFonts,
|
||||
@@ -577,6 +603,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setEnterToSendAI,
|
||||
enterToSendTerminal,
|
||||
setEnterToSendTerminal,
|
||||
defaultSaveToHistory,
|
||||
setDefaultSaveToHistory,
|
||||
leftSidebarWidth,
|
||||
rightPanelWidth,
|
||||
markdownRawMode,
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface WorktreeConfig {
|
||||
branchName: string; // Branch name to use/create
|
||||
createPROnCompletion: boolean; // Create PR when Auto Run finishes
|
||||
prTargetBranch: string; // Target branch for the PR (e.g., 'main')
|
||||
ghPath?: string; // Custom path to gh CLI binary (optional)
|
||||
}
|
||||
|
||||
// Configuration for starting a batch run
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface CreateTabOptions {
|
||||
name?: string | null; // User-defined name (null = show UUID octet)
|
||||
starred?: boolean; // Whether session is starred
|
||||
usageStats?: UsageStats; // Token usage stats
|
||||
saveToHistory?: boolean; // Whether to save synopsis to history after completions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +81,8 @@ export function createTab(session: Session, options: CreateTabOptions = {}): Cre
|
||||
logs = [],
|
||||
name = null,
|
||||
starred = false,
|
||||
usageStats
|
||||
usageStats,
|
||||
saveToHistory = false
|
||||
} = options;
|
||||
|
||||
// Create the new tab with default values
|
||||
@@ -94,7 +96,8 @@ export function createTab(session: Session, options: CreateTabOptions = {}): Cre
|
||||
stagedImages: [],
|
||||
usageStats,
|
||||
createdAt: Date.now(),
|
||||
state: 'idle'
|
||||
state: 'idle',
|
||||
saveToHistory
|
||||
};
|
||||
|
||||
// Update the session with the new tab added and set as active
|
||||
|
||||
32
vitest.config.mts
Normal file
32
vitest.config.mts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/__tests__/setup.ts'],
|
||||
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist', 'release'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'text-summary', 'json', 'html'],
|
||||
reportsDirectory: './coverage',
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
exclude: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'src/__tests__/**',
|
||||
'**/*.d.ts',
|
||||
'src/main/preload.ts', // Electron preload script
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user