mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
added debug support package production
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"clean": "rm -rf dist release node_modules/.vite",
|
"clean": "rm -rf dist release node_modules/.vite",
|
||||||
"postinstall": "electron-rebuild -f -w node-pty",
|
"postinstall": "electron-rebuild -f -w node-pty",
|
||||||
|
"lint": "tsc -p tsconfig.lint.json",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
|
|||||||
@@ -2133,7 +2133,7 @@ describe('Search in markdown with highlighting', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switches to raw mode when searching in rendered markdown', async () => {
|
it('keeps rendered markdown when searching in preview mode', async () => {
|
||||||
render(
|
render(
|
||||||
<FilePreview
|
<FilePreview
|
||||||
file={createMockFile({
|
file={createMockFile({
|
||||||
@@ -2149,6 +2149,9 @@ describe('Search in markdown with highlighting', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Verify we start in preview mode (ReactMarkdown is rendered)
|
||||||
|
expect(screen.getByTestId('react-markdown')).toBeInTheDocument();
|
||||||
|
|
||||||
// Open search
|
// Open search
|
||||||
const container = screen.getByText('test.md').closest('[tabindex="0"]');
|
const container = screen.getByText('test.md').closest('[tabindex="0"]');
|
||||||
fireEvent.keyDown(container!, { key: 'f', metaKey: true });
|
fireEvent.keyDown(container!, { key: 'f', metaKey: true });
|
||||||
@@ -2157,14 +2160,14 @@ describe('Search in markdown with highlighting', () => {
|
|||||||
expect(screen.getByPlaceholderText(/Search in file/)).toBeInTheDocument();
|
expect(screen.getByPlaceholderText(/Search in file/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type search - this should trigger raw mode display with highlights
|
// Type search
|
||||||
const searchInput = screen.getByPlaceholderText(/Search in file/);
|
const searchInput = screen.getByPlaceholderText(/Search in file/);
|
||||||
fireEvent.change(searchInput, { target: { value: 'test' } });
|
fireEvent.change(searchInput, { target: { value: 'test' } });
|
||||||
|
|
||||||
// Wait for component to switch to highlight mode
|
// Verify we stay in preview mode (ReactMarkdown is still rendered)
|
||||||
|
// The search highlights are applied via DOM manipulation, not by switching to raw mode
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// When searching in markdown, it shows raw content with highlights
|
expect(screen.getByTestId('react-markdown')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1/1')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -233,22 +233,6 @@ describe('TerminalOutput', () => {
|
|||||||
expect(userMessageContainer).toBeInTheDocument();
|
expect(userMessageContainer).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows read-only badge for read-only user messages', () => {
|
|
||||||
const logs: LogEntry[] = [
|
|
||||||
createLogEntry({ text: 'Read-only message', source: 'user', readOnly: true }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const session = createDefaultSession({
|
|
||||||
tabs: [{ id: 'tab-1', agentSessionId: 'claude-123', logs, isUnread: false }],
|
|
||||||
activeTabId: 'tab-1',
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = createDefaultProps({ session });
|
|
||||||
render(<TerminalOutput {...props} />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Read-only')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows delivered checkmark for delivered messages', () => {
|
it('shows delivered checkmark for delivered messages', () => {
|
||||||
const logs: LogEntry[] = [
|
const logs: LogEntry[] = [
|
||||||
createLogEntry({ text: 'Delivered message', source: 'user', delivered: true }),
|
createLogEntry({ text: 'Delivered message', source: 'user', delivered: true }),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ vi.mock('lucide-react', () => {
|
|||||||
return new Proxy({}, {
|
return new Proxy({}, {
|
||||||
get(_target, prop: string) {
|
get(_target, prop: string) {
|
||||||
// Ignore internal properties
|
// Ignore internal properties
|
||||||
if (prop === '__esModule' || prop === 'default' || typeof prop === 'symbol') {
|
if (prop === '__esModule' || prop === 'default' || prop === 'then' || typeof prop === 'symbol') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,23 @@ vi.mock('lucide-react', () => {
|
|||||||
}
|
}
|
||||||
return iconCache.get(prop);
|
return iconCache.get(prop);
|
||||||
},
|
},
|
||||||
|
has(_target, prop: string) {
|
||||||
|
if (prop === '__esModule' || prop === 'default' || prop === 'then' || typeof prop === 'symbol') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(_target, prop: string) {
|
||||||
|
if (prop === '__esModule' || prop === 'default' || prop === 'then' || typeof prop === 'symbol') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: false,
|
||||||
|
value: this.get?.(_target, prop),
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { getSessionById } from '../services/storage';
|
import { getSessionById } from '../services/storage';
|
||||||
import { findPlaybookById } from '../services/playbooks';
|
import { findPlaybookById } from '../services/playbooks';
|
||||||
import { runPlaybook as executePlaybook } from '../services/batch-processor';
|
import { runPlaybook as executePlaybook } from '../services/batch-processor';
|
||||||
import { detectClaude } from '../services/agent-spawner';
|
import { detectClaude, detectCodex } from '../services/agent-spawner';
|
||||||
import { emitError } from '../output/jsonl';
|
import { emitError } from '../output/jsonl';
|
||||||
import { formatRunEvent, formatError, formatInfo, formatWarning, RunEvent } from '../output/formatter';
|
import { formatRunEvent, formatError, formatInfo, formatWarning, RunEvent } from '../output/formatter';
|
||||||
import { isSessionBusyWithCli, getCliActivityForSession } from '../../shared/cli-activity';
|
import { isSessionBusyWithCli, getCliActivityForSession } from '../../shared/cli-activity';
|
||||||
@@ -94,17 +94,6 @@ export async function runPlaybook(playbookId: string, options: RunPlaybookOption
|
|||||||
const useJson = options.json;
|
const useJson = options.json;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if Claude is available
|
|
||||||
const claude = await detectClaude();
|
|
||||||
if (!claude.available) {
|
|
||||||
if (useJson) {
|
|
||||||
emitError('Claude Code not found. Please install claude-code CLI.', 'CLAUDE_NOT_FOUND');
|
|
||||||
} else {
|
|
||||||
console.error(formatError('Claude Code not found. Please install claude-code CLI.'));
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let agentId: string;
|
let agentId: string;
|
||||||
let playbook;
|
let playbook;
|
||||||
|
|
||||||
@@ -125,6 +114,37 @@ export async function runPlaybook(playbookId: string, options: RunPlaybookOption
|
|||||||
|
|
||||||
const agent = getSessionById(agentId)!;
|
const agent = getSessionById(agentId)!;
|
||||||
|
|
||||||
|
// Check if agent CLI is available
|
||||||
|
if (agent.toolType === 'codex') {
|
||||||
|
const codex = await detectCodex();
|
||||||
|
if (!codex.available) {
|
||||||
|
if (useJson) {
|
||||||
|
emitError('Codex CLI not found. Please install codex CLI.', 'CODEX_NOT_FOUND');
|
||||||
|
} else {
|
||||||
|
console.error(formatError('Codex CLI not found. Please install codex CLI.'));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else if (agent.toolType === 'claude' || agent.toolType === 'claude-code') {
|
||||||
|
const claude = await detectClaude();
|
||||||
|
if (!claude.available) {
|
||||||
|
if (useJson) {
|
||||||
|
emitError('Claude Code not found. Please install claude-code CLI.', 'CLAUDE_NOT_FOUND');
|
||||||
|
} else {
|
||||||
|
console.error(formatError('Claude Code not found. Please install claude-code CLI.'));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const message = `Agent type "${agent.toolType}" is not supported in CLI batch mode yet.`;
|
||||||
|
if (useJson) {
|
||||||
|
emitError(message, 'AGENT_UNSUPPORTED');
|
||||||
|
} else {
|
||||||
|
console.error(formatError(message));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if agent is busy (either from desktop or another CLI instance)
|
// Check if agent is busy (either from desktop or another CLI instance)
|
||||||
let busyCheck = checkAgentBusy(agent.id, agent.name);
|
let busyCheck = checkAgentBusy(agent.id, agent.name);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// Agent spawner service for CLI
|
// Agent spawner service for CLI
|
||||||
// Spawns Claude Code and parses its output
|
// Spawns agent CLIs (Claude Code, Codex) and parses their output
|
||||||
|
|
||||||
import { spawn, SpawnOptions } from 'child_process';
|
import { spawn, SpawnOptions } from 'child_process';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import type { UsageStats } from '../../shared/types';
|
import type { ToolType, UsageStats } from '../../shared/types';
|
||||||
|
import { CodexOutputParser } from '../../main/parsers/codex-output-parser';
|
||||||
import { getAgentCustomPath } from './storage';
|
import { getAgentCustomPath } from './storage';
|
||||||
|
|
||||||
// Claude Code default command and arguments (same as Electron app)
|
// Claude Code default command and arguments (same as Electron app)
|
||||||
@@ -14,6 +15,13 @@ const CLAUDE_ARGS = ['--print', '--verbose', '--output-format', 'stream-json', '
|
|||||||
// Cached Claude path (resolved once at startup)
|
// Cached Claude path (resolved once at startup)
|
||||||
let cachedClaudePath: string | null = null;
|
let cachedClaudePath: string | null = null;
|
||||||
|
|
||||||
|
// Codex default command and arguments (batch mode)
|
||||||
|
const CODEX_DEFAULT_COMMAND = 'codex';
|
||||||
|
const CODEX_ARGS = ['exec', '--json', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'];
|
||||||
|
|
||||||
|
// Cached Codex path (resolved once at startup)
|
||||||
|
let cachedCodexPath: string | null = null;
|
||||||
|
|
||||||
// Result from spawning an agent
|
// Result from spawning an agent
|
||||||
export interface AgentResult {
|
export interface AgentResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -117,6 +125,35 @@ async function findClaudeInPath(): Promise<string | undefined> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find Codex in PATH using 'which' command
|
||||||
|
*/
|
||||||
|
async function findCodexInPath(): Promise<string | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const env = { ...process.env, PATH: getExpandedPath() };
|
||||||
|
const command = process.platform === 'win32' ? 'where' : 'which';
|
||||||
|
|
||||||
|
const proc = spawn(command, [CODEX_DEFAULT_COMMAND], { env });
|
||||||
|
let stdout = '';
|
||||||
|
|
||||||
|
proc.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0 && stdout.trim()) {
|
||||||
|
resolve(stdout.trim().split('\n')[0]); // First match
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', () => {
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Claude Code is available
|
* Check if Claude Code is available
|
||||||
* First checks for a custom path in settings, then falls back to PATH detection
|
* First checks for a custom path in settings, then falls back to PATH detection
|
||||||
@@ -148,6 +185,33 @@ export async function detectClaude(): Promise<{ available: boolean; path?: strin
|
|||||||
return { available: false };
|
return { available: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Codex CLI is available
|
||||||
|
* First checks for a custom path in settings, then falls back to PATH detection
|
||||||
|
*/
|
||||||
|
export async function detectCodex(): Promise<{ available: boolean; path?: string; source?: 'settings' | 'path' }> {
|
||||||
|
if (cachedCodexPath) {
|
||||||
|
return { available: true, path: cachedCodexPath, source: 'settings' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const customPath = getAgentCustomPath('codex');
|
||||||
|
if (customPath) {
|
||||||
|
if (await isExecutable(customPath)) {
|
||||||
|
cachedCodexPath = customPath;
|
||||||
|
return { available: true, path: customPath, source: 'settings' };
|
||||||
|
}
|
||||||
|
console.error(`Warning: Custom Codex path "${customPath}" is not executable, falling back to PATH detection`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathResult = await findCodexInPath();
|
||||||
|
if (pathResult) {
|
||||||
|
cachedCodexPath = pathResult;
|
||||||
|
return { available: true, path: pathResult, source: 'path' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { available: false };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the resolved Claude command/path for spawning
|
* Get the resolved Claude command/path for spawning
|
||||||
* Uses cached path from detectClaude() or falls back to default command
|
* Uses cached path from detectClaude() or falls back to default command
|
||||||
@@ -156,10 +220,18 @@ export function getClaudeCommand(): string {
|
|||||||
return cachedClaudePath || CLAUDE_DEFAULT_COMMAND;
|
return cachedClaudePath || CLAUDE_DEFAULT_COMMAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resolved Codex command/path for spawning
|
||||||
|
* Uses cached path from detectCodex() or falls back to default command
|
||||||
|
*/
|
||||||
|
export function getCodexCommand(): string {
|
||||||
|
return cachedCodexPath || CODEX_DEFAULT_COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawn Claude Code with a prompt and return the result
|
* Spawn Claude Code with a prompt and return the result
|
||||||
*/
|
*/
|
||||||
export async function spawnAgent(
|
async function spawnClaudeAgent(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
agentSessionId?: string
|
agentSessionId?: string
|
||||||
@@ -309,6 +381,157 @@ export async function spawnAgent(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeUsageStats(current: UsageStats | undefined, next: {
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheCreationTokens?: number;
|
||||||
|
costUsd?: number;
|
||||||
|
contextWindow?: number;
|
||||||
|
reasoningTokens?: number;
|
||||||
|
}): UsageStats {
|
||||||
|
const merged: UsageStats = {
|
||||||
|
inputTokens: (current?.inputTokens || 0) + (next.inputTokens || 0),
|
||||||
|
outputTokens: (current?.outputTokens || 0) + (next.outputTokens || 0),
|
||||||
|
cacheReadInputTokens: (current?.cacheReadInputTokens || 0) + (next.cacheReadTokens || 0),
|
||||||
|
cacheCreationInputTokens: (current?.cacheCreationInputTokens || 0) + (next.cacheCreationTokens || 0),
|
||||||
|
totalCostUsd: (current?.totalCostUsd || 0) + (next.costUsd || 0),
|
||||||
|
contextWindow: Math.max(current?.contextWindow || 0, next.contextWindow || 0),
|
||||||
|
reasoningTokens: (current?.reasoningTokens || 0) + (next.reasoningTokens || 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!next.reasoningTokens && !current?.reasoningTokens) {
|
||||||
|
delete merged.reasoningTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn Codex with a prompt and return the result
|
||||||
|
*/
|
||||||
|
async function spawnCodexAgent(
|
||||||
|
cwd: string,
|
||||||
|
prompt: string,
|
||||||
|
agentSessionId?: string
|
||||||
|
): Promise<AgentResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const env: NodeJS.ProcessEnv = {
|
||||||
|
...process.env,
|
||||||
|
PATH: getExpandedPath(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const args = [...CODEX_ARGS];
|
||||||
|
args.push('-C', cwd);
|
||||||
|
|
||||||
|
if (agentSessionId) {
|
||||||
|
args.push('resume', agentSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('--', prompt);
|
||||||
|
|
||||||
|
const options: SpawnOptions = {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const codexCommand = getCodexCommand();
|
||||||
|
const child = spawn(codexCommand, args, options);
|
||||||
|
|
||||||
|
const parser = new CodexOutputParser();
|
||||||
|
let jsonBuffer = '';
|
||||||
|
let result: string | undefined;
|
||||||
|
let sessionId: string | undefined;
|
||||||
|
let usageStats: UsageStats | undefined;
|
||||||
|
let stderr = '';
|
||||||
|
let errorText: string | undefined;
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data: Buffer) => {
|
||||||
|
jsonBuffer += data.toString();
|
||||||
|
const lines = jsonBuffer.split('\n');
|
||||||
|
jsonBuffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
const event = parser.parseJsonLine(line);
|
||||||
|
if (!event) continue;
|
||||||
|
|
||||||
|
if (event.type === 'init' && event.sessionId && !sessionId) {
|
||||||
|
sessionId = event.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'result' && event.text) {
|
||||||
|
result = result ? `${result}\n${event.text}` : event.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'error' && event.text && !errorText) {
|
||||||
|
errorText = event.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = parser.extractUsage(event);
|
||||||
|
if (usage) {
|
||||||
|
usageStats = mergeUsageStats(usageStats, usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data: Buffer) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin?.end();
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0 && !errorText) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
response: result,
|
||||||
|
agentSessionId: sessionId,
|
||||||
|
usageStats,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: errorText || stderr || `Process exited with code ${code}`,
|
||||||
|
agentSessionId: sessionId,
|
||||||
|
usageStats,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to spawn Codex: ${error.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn an agent with a prompt and return the result
|
||||||
|
*/
|
||||||
|
export async function spawnAgent(
|
||||||
|
toolType: ToolType,
|
||||||
|
cwd: string,
|
||||||
|
prompt: string,
|
||||||
|
agentSessionId?: string
|
||||||
|
): Promise<AgentResult> {
|
||||||
|
if (toolType === 'codex') {
|
||||||
|
return spawnCodexAgent(cwd, prompt, agentSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolType === 'claude' || toolType === 'claude-code') {
|
||||||
|
return spawnClaudeAgent(cwd, prompt, agentSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Unsupported agent type for batch mode: ${toolType}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a markdown document and count unchecked tasks
|
* Read a markdown document and count unchecked tasks
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ export async function* runPlaybook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn agent with combined prompt + document
|
// Spawn agent with combined prompt + document
|
||||||
const result = await spawnAgent(session.cwd, finalPrompt);
|
const result = await spawnAgent(session.toolType, session.cwd, finalPrompt);
|
||||||
|
|
||||||
const elapsedMs = Date.now() - taskStartTime;
|
const elapsedMs = Date.now() - taskStartTime;
|
||||||
|
|
||||||
@@ -497,6 +497,7 @@ export async function* runPlaybook(
|
|||||||
if (result.success && result.agentSessionId) {
|
if (result.success && result.agentSessionId) {
|
||||||
// Request synopsis from the agent
|
// Request synopsis from the agent
|
||||||
const synopsisResult = await spawnAgent(
|
const synopsisResult = await spawnAgent(
|
||||||
|
session.toolType,
|
||||||
session.cwd,
|
session.cwd,
|
||||||
BATCH_SYNOPSIS_PROMPT,
|
BATCH_SYNOPSIS_PROMPT,
|
||||||
result.agentSessionId
|
result.agentSessionId
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ Maestro is an Electron desktop application for managing multiple AI coding assis
|
|||||||
- **Git Branch:** {{GIT_BRANCH}}
|
- **Git Branch:** {{GIT_BRANCH}}
|
||||||
- **Session ID:** {{AGENT_SESSION_ID}}
|
- **Session ID:** {{AGENT_SESSION_ID}}
|
||||||
|
|
||||||
|
## Auto-run Documents
|
||||||
|
|
||||||
|
When a user wants an auto-run document, create a detailed multi-document, multi-point Markdown implementation plan in the `{{AUTORUN_FOLDER}}` folder. Use the format `$PREFIX-X.md`, where `X` is the phase number and `$PREFIX` is the effort name. Break phases by relevant context; do not mix unrelated task results in the same document. If working within a file, group and fix all type issues in that file together. If working with an MCP, keep all related tasks in the same document. Each task must be written as `- [ ] ...` so auto-run can execute and check them off with comments on completion. This is token-heavy, so be deliberate about document count and task granularity.
|
||||||
|
|
||||||
## Critical Directive: Directory Restrictions
|
## Critical Directive: Directory Restrictions
|
||||||
|
|
||||||
**You MUST only write files within your assigned working directory:**
|
**You MUST only write files within your assigned working directory:**
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ The user is continuing a previous planning session. Below are the existing Auto
|
|||||||
|
|
||||||
{{EXISTING_DOCS}}
|
{{EXISTING_DOCS}}
|
||||||
|
|
||||||
|
## Auto-run Documents
|
||||||
|
|
||||||
|
When a user wants an auto-run document, create a detailed multi-document, multi-point Markdown implementation plan in the `{{AUTORUN_FOLDER}}` folder. Use the format `$PREFIX-X.md`, where `X` is the phase number and `$PREFIX` is the effort name. Break phases by relevant context; do not mix unrelated task results in the same document. If working within a file, group and fix all type issues in that file together. If working with an MCP, keep all related tasks in the same document. Each task must be written as `- [ ] ...` so auto-run can execute and check them off with comments on completion. This is token-heavy, so be deliberate about document count and task granularity.
|
||||||
|
|
||||||
**Important:** When continuing from existing docs:
|
**Important:** When continuing from existing docs:
|
||||||
- Start with higher confidence (60-70%) since you already have context
|
- Start with higher confidence (60-70%) since you already have context
|
||||||
- Review the existing plans and ask if anything has changed or needs updating
|
- Review the existing plans and ask if anything has changed or needs updating
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ You will ONLY create or modify files within this directory:
|
|||||||
|
|
||||||
Do not reference, create, or modify files outside this path.
|
Do not reference, create, or modify files outside this path.
|
||||||
|
|
||||||
|
## Auto-run Documents
|
||||||
|
|
||||||
|
When a user wants an auto-run document, create a detailed multi-document, multi-point Markdown implementation plan in the `{{AUTORUN_FOLDER}}` folder. Use the format `$PREFIX-X.md`, where `X` is the phase number and `$PREFIX` is the effort name. Break phases by relevant context; do not mix unrelated task results in the same document. If working within a file, group and fix all type issues in that file together. If working with an MCP, keep all related tasks in the same document. Each task must be written as `- [ ] ...` so auto-run can execute and check them off with comments on completion. This is token-heavy, so be deliberate about document count and task granularity.
|
||||||
|
|
||||||
## Your Goal
|
## Your Goal
|
||||||
|
|
||||||
Through a brief, focused conversation:
|
Through a brief, focused conversation:
|
||||||
|
|||||||
@@ -178,32 +178,6 @@ export function AgentErrorModal({
|
|||||||
{new Date(error.timestamp).toLocaleTimeString()}
|
{new Date(error.timestamp).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recovery hint - different message based on whether there are actions */}
|
|
||||||
{error.recoverable && recoveryActions.length > 0 && (
|
|
||||||
<div
|
|
||||||
className="text-xs p-3 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${errorColor}15`,
|
|
||||||
color: theme.colors.textMain,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="font-medium">Tip:</span> This error can be resolved.
|
|
||||||
Choose a recovery action below.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error.recoverable && recoveryActions.length === 0 && (
|
|
||||||
<div
|
|
||||||
className="text-xs p-3 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${errorColor}15`,
|
|
||||||
color: theme.colors.textMain,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="font-medium">Tip:</span> Simply send another message to
|
|
||||||
continue, or start a new session.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Collapsible JSON Details */}
|
{/* Collapsible JSON Details */}
|
||||||
{hasJsonDetails && (
|
{hasJsonDetails && (
|
||||||
<div className="border rounded" style={{ borderColor: theme.colors.border }}>
|
<div className="border rounded" style={{ borderColor: theme.colors.border }}>
|
||||||
|
|||||||
@@ -731,89 +731,29 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
|||||||
};
|
};
|
||||||
}, [searchQuery, file.content, isMarkdown, isImage, theme.colors.accent]);
|
}, [searchQuery, file.content, isMarkdown, isImage, theme.colors.accent]);
|
||||||
|
|
||||||
// Highlight search matches in markdown preview (DOM-based highlighting after render)
|
// Count search matches in markdown preview mode (no DOM manipulation to avoid React conflicts)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMarkdown || markdownEditMode || !searchQuery.trim() || !markdownContainerRef.current) {
|
if (!isMarkdown || markdownEditMode || !searchQuery.trim()) {
|
||||||
|
if (isMarkdown && !markdownEditMode) {
|
||||||
|
setTotalMatches(0);
|
||||||
|
setCurrentMatchIndex(0);
|
||||||
|
matchElementsRef.current = [];
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = markdownContainerRef.current;
|
// Count matches in the raw content
|
||||||
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
|
|
||||||
// Collect all text nodes
|
|
||||||
let node;
|
|
||||||
while ((node = walker.nextNode())) {
|
|
||||||
textNodes.push(node as Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape regex special characters
|
|
||||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
const regex = new RegExp(escapedQuery, 'gi');
|
const regex = new RegExp(escapedQuery, 'gi');
|
||||||
const matchElements: HTMLElement[] = [];
|
const matches = file.content.match(regex);
|
||||||
|
const count = matches ? matches.length : 0;
|
||||||
|
|
||||||
// Highlight matches using safe DOM methods
|
setTotalMatches(count);
|
||||||
textNodes.forEach(textNode => {
|
if (count > 0 && currentMatchIndex >= count) {
|
||||||
const text = textNode.textContent || '';
|
setCurrentMatchIndex(0);
|
||||||
const matches = text.match(regex);
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
let lastIndex = 0;
|
|
||||||
|
|
||||||
text.replace(regex, (match, offset) => {
|
|
||||||
// Add text before match
|
|
||||||
if (offset > lastIndex) {
|
|
||||||
fragment.appendChild(document.createTextNode(text.substring(lastIndex, offset)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add highlighted match
|
|
||||||
const mark = document.createElement('mark');
|
|
||||||
mark.style.backgroundColor = '#ffd700';
|
|
||||||
mark.style.color = '#000';
|
|
||||||
mark.style.padding = '0 2px';
|
|
||||||
mark.style.borderRadius = '2px';
|
|
||||||
mark.className = 'search-match-md';
|
|
||||||
mark.textContent = match;
|
|
||||||
fragment.appendChild(mark);
|
|
||||||
matchElements.push(mark);
|
|
||||||
|
|
||||||
lastIndex = offset + match.length;
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add remaining text
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
|
|
||||||
}
|
|
||||||
|
|
||||||
textNode.parentNode?.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store match elements and update count
|
|
||||||
matchElementsRef.current = matchElements;
|
|
||||||
setTotalMatches(matchElements.length);
|
|
||||||
setCurrentMatchIndex(matchElements.length > 0 ? 0 : -1);
|
|
||||||
|
|
||||||
// Highlight first match with different color and scroll to it
|
|
||||||
if (matchElements.length > 0) {
|
|
||||||
matchElements[0].style.backgroundColor = theme.colors.accent;
|
|
||||||
matchElements[0].style.color = '#fff';
|
|
||||||
matchElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
}
|
||||||
|
matchElementsRef.current = [];
|
||||||
// Cleanup function to remove highlights
|
}, [searchQuery, file.content, isMarkdown, markdownEditMode, currentMatchIndex]);
|
||||||
return () => {
|
|
||||||
container.querySelectorAll('mark.search-match-md').forEach(mark => {
|
|
||||||
const parent = mark.parentNode;
|
|
||||||
if (parent) {
|
|
||||||
parent.replaceChild(document.createTextNode(mark.textContent || ''), mark);
|
|
||||||
parent.normalize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [searchQuery, file.content, isMarkdown, markdownEditMode, theme.colors.accent]);
|
|
||||||
|
|
||||||
const [copyNotificationMessage, setCopyNotificationMessage] = useState('');
|
const [copyNotificationMessage, setCopyNotificationMessage] = useState('');
|
||||||
|
|
||||||
@@ -851,47 +791,53 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
|||||||
// Navigate to next search match
|
// Navigate to next search match
|
||||||
const goToNextMatch = () => {
|
const goToNextMatch = () => {
|
||||||
if (totalMatches === 0) return;
|
if (totalMatches === 0) return;
|
||||||
const matches = matchElementsRef.current;
|
|
||||||
|
|
||||||
// Reset current match highlight
|
|
||||||
if (matches[currentMatchIndex]) {
|
|
||||||
matches[currentMatchIndex].style.backgroundColor = '#ffd700';
|
|
||||||
matches[currentMatchIndex].style.color = '#000';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next match (wrap around)
|
// Move to next match (wrap around)
|
||||||
const nextIndex = (currentMatchIndex + 1) % totalMatches;
|
const nextIndex = (currentMatchIndex + 1) % totalMatches;
|
||||||
setCurrentMatchIndex(nextIndex);
|
setCurrentMatchIndex(nextIndex);
|
||||||
|
|
||||||
// Highlight new current match and scroll to it
|
// For code files, handle DOM-based highlighting
|
||||||
if (matches[nextIndex]) {
|
const matches = matchElementsRef.current;
|
||||||
matches[nextIndex].style.backgroundColor = theme.colors.accent;
|
if (matches.length > 0) {
|
||||||
matches[nextIndex].style.color = '#fff';
|
// Reset previous highlight
|
||||||
matches[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (matches[currentMatchIndex]) {
|
||||||
|
matches[currentMatchIndex].style.backgroundColor = '#ffd700';
|
||||||
|
matches[currentMatchIndex].style.color = '#000';
|
||||||
|
}
|
||||||
|
// Highlight new current match and scroll to it
|
||||||
|
if (matches[nextIndex]) {
|
||||||
|
matches[nextIndex].style.backgroundColor = theme.colors.accent;
|
||||||
|
matches[nextIndex].style.color = '#fff';
|
||||||
|
matches[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// For markdown edit mode, the effect will handle selecting text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to previous search match
|
// Navigate to previous search match
|
||||||
const goToPrevMatch = () => {
|
const goToPrevMatch = () => {
|
||||||
if (totalMatches === 0) return;
|
if (totalMatches === 0) return;
|
||||||
const matches = matchElementsRef.current;
|
|
||||||
|
|
||||||
// Reset current match highlight
|
|
||||||
if (matches[currentMatchIndex]) {
|
|
||||||
matches[currentMatchIndex].style.backgroundColor = '#ffd700';
|
|
||||||
matches[currentMatchIndex].style.color = '#000';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to previous match (wrap around)
|
// Move to previous match (wrap around)
|
||||||
const prevIndex = (currentMatchIndex - 1 + totalMatches) % totalMatches;
|
const prevIndex = (currentMatchIndex - 1 + totalMatches) % totalMatches;
|
||||||
setCurrentMatchIndex(prevIndex);
|
setCurrentMatchIndex(prevIndex);
|
||||||
|
|
||||||
// Highlight new current match and scroll to it
|
// For code files, handle DOM-based highlighting
|
||||||
if (matches[prevIndex]) {
|
const matches = matchElementsRef.current;
|
||||||
matches[prevIndex].style.backgroundColor = theme.colors.accent;
|
if (matches.length > 0) {
|
||||||
matches[prevIndex].style.color = '#fff';
|
// Reset previous highlight
|
||||||
matches[prevIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (matches[currentMatchIndex]) {
|
||||||
|
matches[currentMatchIndex].style.backgroundColor = '#ffd700';
|
||||||
|
matches[currentMatchIndex].style.color = '#000';
|
||||||
|
}
|
||||||
|
// Highlight new current match and scroll to it
|
||||||
|
if (matches[prevIndex]) {
|
||||||
|
matches[prevIndex].style.backgroundColor = theme.colors.accent;
|
||||||
|
matches[prevIndex].style.color = '#fff';
|
||||||
|
matches[prevIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// For markdown edit mode, the effect will handle selecting text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format shortcut keys for display
|
// Format shortcut keys for display
|
||||||
@@ -901,25 +847,6 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
|||||||
return formatShortcutKeys(shortcut.keys);
|
return formatShortcutKeys(shortcut.keys);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to current match for markdown content (using stored match elements)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMarkdown && searchQuery.trim() && !markdownEditMode) {
|
|
||||||
const matches = matchElementsRef.current;
|
|
||||||
if (matches.length > 0 && currentMatchIndex >= 0 && currentMatchIndex < matches.length) {
|
|
||||||
matches.forEach((mark, i) => {
|
|
||||||
if (i === currentMatchIndex) {
|
|
||||||
mark.style.backgroundColor = theme.colors.accent;
|
|
||||||
mark.style.color = '#fff';
|
|
||||||
mark.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
} else {
|
|
||||||
mark.style.backgroundColor = '#ffd700';
|
|
||||||
mark.style.color = '#000';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [currentMatchIndex, isMarkdown, markdownEditMode, searchQuery, theme.colors.accent]);
|
|
||||||
|
|
||||||
// Handle search in markdown edit mode - jump to and select the match in textarea
|
// Handle search in markdown edit mode - jump to and select the match in textarea
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMarkdown || !markdownEditMode || !searchQuery.trim() || !textareaRef.current) {
|
if (!isMarkdown || !markdownEditMode || !searchQuery.trim() || !textareaRef.current) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef, useEffect, useCallback, useMemo, useState } from 'react';
|
import { useRef, useEffect, useCallback, useMemo, useState } from 'react';
|
||||||
import { BookOpen, Eye, FileText, Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Eye, FileText, Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import type { GroupChatMessage, GroupChatParticipant, GroupChatState, Theme } from '../types';
|
import type { GroupChatMessage, GroupChatParticipant, GroupChatState, Theme } from '../types';
|
||||||
import { MarkdownRenderer } from './MarkdownRenderer';
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
||||||
import { stripMarkdown } from '../utils/textProcessing';
|
import { stripMarkdown } from '../utils/textProcessing';
|
||||||
@@ -198,7 +198,7 @@ export function GroupChatMessages({
|
|||||||
|
|
||||||
{/* Message bubble */}
|
{/* Message bubble */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 min-w-0 p-4 pb-10 ${isUser && msg.readOnly ? 'pt-8' : ''} rounded-xl border ${isUser ? 'rounded-tr-none' : 'rounded-tl-none'} relative overflow-hidden`}
|
className={`flex-1 min-w-0 p-4 pb-10 rounded-xl border ${isUser ? 'rounded-tr-none' : 'rounded-tl-none'} relative overflow-hidden`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isUser
|
backgroundColor: isUser
|
||||||
? `color-mix(in srgb, ${theme.colors.accent} 20%, ${theme.colors.bgSidebar})`
|
? `color-mix(in srgb, ${theme.colors.accent} 20%, ${theme.colors.bgSidebar})`
|
||||||
@@ -211,24 +211,6 @@ export function GroupChatMessages({
|
|||||||
color: theme.colors.textMain,
|
color: theme.colors.textMain,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Read-only badge for user messages */}
|
|
||||||
{isUser && msg.readOnly && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<span
|
|
||||||
className="flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${theme.colors.warning}25`,
|
|
||||||
color: theme.colors.warning,
|
|
||||||
border: `1px solid ${theme.colors.warning}50`
|
|
||||||
}}
|
|
||||||
title="Sent in read-only mode"
|
|
||||||
>
|
|
||||||
<BookOpen className="w-3 h-3" />
|
|
||||||
<span>Read-only</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sender label for non-user messages */}
|
{/* Sender label for non-user messages */}
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -123,9 +123,19 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
|||||||
? hasCapability('supportsImageInputOnResume')
|
? hasCapability('supportsImageInputOnResume')
|
||||||
: hasCapability('supportsImageInput');
|
: hasCapability('supportsImageInput');
|
||||||
|
|
||||||
// Check if we're in read-only mode (auto mode OR manual toggle - Claude will be in plan mode)
|
// Check if we're in read-only mode (manual toggle only - Claude will be in plan mode)
|
||||||
const isAutoReadOnly = isAutoModeActive && session.inputMode === 'ai';
|
// NOTE: Auto Run no longer forces read-only mode. Instead:
|
||||||
const isReadOnlyMode = isAutoReadOnly || (tabReadOnlyMode && session.inputMode === 'ai');
|
// - Yellow border shows during Auto Run to indicate queuing will happen for write messages
|
||||||
|
// - User can freely toggle read-only mode during Auto Run
|
||||||
|
// - If read-only is ON: message sends immediately (parallel read-only operations allowed)
|
||||||
|
// - If read-only is OFF: message queues until Auto Run completes (prevents file conflicts)
|
||||||
|
const isReadOnlyMode = tabReadOnlyMode && session.inputMode === 'ai';
|
||||||
|
|
||||||
|
// Check if Auto Run is active - used for yellow border indication (queuing will happen for write messages)
|
||||||
|
const isAutoRunActive = isAutoModeActive && session.inputMode === 'ai';
|
||||||
|
|
||||||
|
// Show yellow border when: read-only mode is on OR Auto Run is active (both indicate special input handling)
|
||||||
|
const showQueueingBorder = isReadOnlyMode || isAutoRunActive;
|
||||||
|
|
||||||
// Filter slash commands based on input and current mode
|
// Filter slash commands based on input and current mode
|
||||||
const isTerminalMode = session.inputMode === 'terminal';
|
const isTerminalMode = session.inputMode === 'terminal';
|
||||||
@@ -539,8 +549,8 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
|||||||
<div
|
<div
|
||||||
className="flex-1 relative border rounded-lg bg-opacity-50 flex flex-col"
|
className="flex-1 relative border rounded-lg bg-opacity-50 flex flex-col"
|
||||||
style={{
|
style={{
|
||||||
borderColor: isReadOnlyMode ? theme.colors.warning : theme.colors.border,
|
borderColor: showQueueingBorder ? theme.colors.warning : theme.colors.border,
|
||||||
backgroundColor: isReadOnlyMode ? `${theme.colors.warning}15` : theme.colors.bgMain
|
backgroundColor: showQueueingBorder ? `${theme.colors.warning}15` : theme.colors.bgMain
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@@ -704,14 +714,11 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Read-only mode toggle - AI mode only, if agent supports it */}
|
{/* Read-only mode toggle - AI mode only, if agent supports it */}
|
||||||
{/* When AutoRun is active, pill is locked to enabled state */}
|
{/* User can freely toggle read-only during Auto Run */}
|
||||||
{session.inputMode === 'ai' && onToggleTabReadOnlyMode && hasCapability('supportsReadOnlyMode') && (
|
{session.inputMode === 'ai' && onToggleTabReadOnlyMode && hasCapability('supportsReadOnlyMode') && (
|
||||||
<button
|
<button
|
||||||
onClick={isAutoReadOnly ? undefined : onToggleTabReadOnlyMode}
|
onClick={onToggleTabReadOnlyMode}
|
||||||
disabled={isAutoReadOnly}
|
className={`flex items-center gap-1.5 text-[10px] px-2 py-1 rounded-full cursor-pointer transition-all ${
|
||||||
className={`flex items-center gap-1.5 text-[10px] px-2 py-1 rounded-full transition-all ${
|
|
||||||
isAutoReadOnly ? 'cursor-not-allowed' : 'cursor-pointer'
|
|
||||||
} ${
|
|
||||||
isReadOnlyMode ? '' : 'opacity-40 hover:opacity-70'
|
isReadOnlyMode ? '' : 'opacity-40 hover:opacity-70'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -719,7 +726,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
|||||||
color: isReadOnlyMode ? theme.colors.warning : theme.colors.textDim,
|
color: isReadOnlyMode ? theme.colors.warning : theme.colors.textDim,
|
||||||
border: isReadOnlyMode ? `1px solid ${theme.colors.warning}50` : '1px solid transparent'
|
border: isReadOnlyMode ? `1px solid ${theme.colors.warning}50` : '1px solid transparent'
|
||||||
}}
|
}}
|
||||||
title={isAutoReadOnly ? "Read-only mode locked during AutoRun" : "Toggle read-only mode (agent won't modify files)"}
|
title="Toggle read-only mode (agent won't modify files)"
|
||||||
>
|
>
|
||||||
<Eye className="w-3 h-3" />
|
<Eye className="w-3 h-3" />
|
||||||
<span>Read-only</span>
|
<span>Read-only</span>
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ const LogItemComponent = memo(({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex-1 min-w-0 p-4 pb-10 ${isUserMessage && log.readOnly ? 'pt-8' : ''} rounded-xl border ${isUserMessage ? 'rounded-tr-none' : 'rounded-tl-none'} relative overflow-hidden`}
|
<div className={`flex-1 min-w-0 p-4 pb-10 rounded-xl border ${isUserMessage ? 'rounded-tr-none' : 'rounded-tl-none'} relative overflow-hidden`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isUserMessage
|
backgroundColor: isUserMessage
|
||||||
? isAIMode
|
? isAIMode
|
||||||
@@ -327,23 +327,6 @@ const LogItemComponent = memo(({
|
|||||||
? theme.colors.accent + '40'
|
? theme.colors.accent + '40'
|
||||||
: (log.source === 'stderr' || log.source === 'error') ? theme.colors.error : theme.colors.border
|
: (log.source === 'stderr' || log.source === 'error') ? theme.colors.error : theme.colors.border
|
||||||
}}>
|
}}>
|
||||||
{/* Read-only badge - top right of message for user messages sent in read-only mode */}
|
|
||||||
{isUserMessage && log.readOnly && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<span
|
|
||||||
className="flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${theme.colors.warning}25`,
|
|
||||||
color: theme.colors.warning,
|
|
||||||
border: `1px solid ${theme.colors.warning}50`
|
|
||||||
}}
|
|
||||||
title="Sent in read-only mode (Claude won't modify files)"
|
|
||||||
>
|
|
||||||
<Eye className="w-3 h-3" />
|
|
||||||
<span>Read-only</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Local filter icon for system output only */}
|
{/* Local filter icon for system output only */}
|
||||||
{log.source !== 'user' && isTerminal && (
|
{log.source !== 'user' && isTerminal && (
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-2">
|
<div className="absolute top-2 right-2 flex items-center gap-2">
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import { getBadgeForTime, getNextBadge, formatTimeRemaining } from '../constants
|
|||||||
import { autorunSynopsisPrompt } from '../../prompts';
|
import { autorunSynopsisPrompt } from '../../prompts';
|
||||||
import { parseSynopsis } from '../../shared/synopsis';
|
import { parseSynopsis } from '../../shared/synopsis';
|
||||||
|
|
||||||
// Regex to count unchecked markdown checkboxes: - [ ] task
|
// Regex to count unchecked markdown checkboxes: - [ ] task (also * [ ])
|
||||||
const UNCHECKED_TASK_REGEX = /^[\s]*-\s*\[\s*\]\s*.+$/gm;
|
const UNCHECKED_TASK_REGEX = /^[\s]*[-*]\s*\[\s*\]\s*.+$/gm;
|
||||||
|
|
||||||
|
// Regex to count checked markdown checkboxes: - [x] task (also * [x])
|
||||||
|
const CHECKED_TASK_COUNT_REGEX = /^[\s]*[-*]\s*\[[xX✓✔]\]\s*.+$/gm;
|
||||||
|
|
||||||
// Regex to match checked markdown checkboxes for reset-on-completion
|
// Regex to match checked markdown checkboxes for reset-on-completion
|
||||||
// Matches both [x] and [X] with various checkbox formats (standard and GitHub-style)
|
// Matches both [x] and [X] with various checkbox formats (standard and GitHub-style)
|
||||||
const CHECKED_TASK_REGEX = /^(\s*-\s*)\[[xX✓✔]\]/gm;
|
const CHECKED_TASK_REGEX = /^(\s*[-*]\s*)\[[xX✓✔]\]/gm;
|
||||||
|
|
||||||
// Default empty batch state
|
// Default empty batch state
|
||||||
const DEFAULT_BATCH_STATE: BatchRunState = {
|
const DEFAULT_BATCH_STATE: BatchRunState = {
|
||||||
@@ -197,6 +200,14 @@ export function countUnfinishedTasks(content: string): number {
|
|||||||
return matches ? matches.length : 0;
|
return matches ? matches.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count checked tasks in markdown content
|
||||||
|
*/
|
||||||
|
function countCheckedTasks(content: string): number {
|
||||||
|
const matches = content.match(CHECKED_TASK_COUNT_REGEX);
|
||||||
|
return matches ? matches.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uncheck all markdown checkboxes in content (for reset-on-completion)
|
* Uncheck all markdown checkboxes in content (for reset-on-completion)
|
||||||
* Converts all - [x] to - [ ] (case insensitive)
|
* Converts all - [x] to - [ ] (case insensitive)
|
||||||
@@ -708,6 +719,8 @@ ${docList}
|
|||||||
|
|
||||||
// Read document and count tasks
|
// Read document and count tasks
|
||||||
let { taskCount: remainingTasks, content: docContent } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
let { taskCount: remainingTasks, content: docContent } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
||||||
|
let docCheckedCount = countCheckedTasks(docContent);
|
||||||
|
let docTasksTotal = remainingTasks;
|
||||||
|
|
||||||
// Handle documents with no unchecked tasks
|
// Handle documents with no unchecked tasks
|
||||||
if (remainingTasks === 0) {
|
if (remainingTasks === 0) {
|
||||||
@@ -756,7 +769,7 @@ ${docList}
|
|||||||
[sessionId]: {
|
[sessionId]: {
|
||||||
...prev[sessionId],
|
...prev[sessionId],
|
||||||
currentDocumentIndex: docIndex,
|
currentDocumentIndex: docIndex,
|
||||||
currentDocTasksTotal: remainingTasks,
|
currentDocTasksTotal: docTasksTotal,
|
||||||
currentDocTasksCompleted: 0
|
currentDocTasksCompleted: 0
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -820,8 +833,11 @@ ${docList}
|
|||||||
|
|
||||||
// Re-read document to get updated task count and content
|
// Re-read document to get updated task count and content
|
||||||
const { taskCount: newRemainingTasks, content: contentAfterTask } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
const { taskCount: newRemainingTasks, content: contentAfterTask } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
||||||
// Calculate tasks completed - ensure it's never negative (Claude may have added tasks)
|
const newCheckedCount = countCheckedTasks(contentAfterTask);
|
||||||
const tasksCompletedThisRun = Math.max(0, remainingTasks - newRemainingTasks);
|
// Calculate tasks completed based on newly checked tasks.
|
||||||
|
// This remains accurate even if new unchecked tasks are added.
|
||||||
|
const tasksCompletedThisRun = Math.max(0, newCheckedCount - docCheckedCount);
|
||||||
|
const addedUncheckedTasks = Math.max(0, newRemainingTasks - remainingTasks);
|
||||||
|
|
||||||
// Detect stalling: if document content is unchanged and no tasks were checked off
|
// Detect stalling: if document content is unchanged and no tasks were checked off
|
||||||
const documentUnchanged = contentBeforeTask === contentAfterTask;
|
const documentUnchanged = contentBeforeTask === contentAfterTask;
|
||||||
@@ -855,18 +871,30 @@ ${docList}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update progress state
|
// Update progress state
|
||||||
updateBatchStateAndBroadcast(sessionId, prev => ({
|
if (addedUncheckedTasks > 0) {
|
||||||
...prev,
|
docTasksTotal += addedUncheckedTasks;
|
||||||
[sessionId]: {
|
}
|
||||||
...prev[sessionId],
|
|
||||||
currentDocTasksCompleted: docTasksCompleted,
|
updateBatchStateAndBroadcast(sessionId, prev => {
|
||||||
completedTasksAcrossAllDocs: totalCompletedTasks,
|
const prevState = prev[sessionId];
|
||||||
// Legacy fields
|
const nextTotalAcrossAllDocs = Math.max(0, prevState.totalTasksAcrossAllDocs + addedUncheckedTasks);
|
||||||
completedTasks: totalCompletedTasks,
|
const nextTotalTasks = Math.max(0, prevState.totalTasks + addedUncheckedTasks);
|
||||||
currentTaskIndex: totalCompletedTasks,
|
return {
|
||||||
sessionIds: [...(prev[sessionId]?.sessionIds || []), result.agentSessionId || '']
|
...prev,
|
||||||
}
|
[sessionId]: {
|
||||||
}));
|
...prevState,
|
||||||
|
currentDocTasksCompleted: docTasksCompleted,
|
||||||
|
currentDocTasksTotal: docTasksTotal,
|
||||||
|
completedTasksAcrossAllDocs: totalCompletedTasks,
|
||||||
|
totalTasksAcrossAllDocs: nextTotalAcrossAllDocs,
|
||||||
|
// Legacy fields
|
||||||
|
completedTasks: totalCompletedTasks,
|
||||||
|
totalTasks: nextTotalTasks,
|
||||||
|
currentTaskIndex: totalCompletedTasks,
|
||||||
|
sessionIds: [...(prevState?.sessionIds || []), result.agentSessionId || '']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Generate synopsis for successful tasks with an agent session
|
// Generate synopsis for successful tasks with an agent session
|
||||||
let shortSummary = `[${docEntry.filename}] Task completed`;
|
let shortSummary = `[${docEntry.filename}] Task completed`;
|
||||||
@@ -977,6 +1005,7 @@ ${docList}
|
|||||||
break; // Break out of the inner while loop for this document
|
break; // Break out of the inner while loop for this document
|
||||||
}
|
}
|
||||||
|
|
||||||
|
docCheckedCount = newCheckedCount;
|
||||||
remainingTasks = newRemainingTasks;
|
remainingTasks = newRemainingTasks;
|
||||||
console.log(`[BatchProcessor] Document ${docEntry.filename}: ${remainingTasks} tasks remaining`);
|
console.log(`[BatchProcessor] Document ${docEntry.filename}: ${remainingTasks} tasks remaining`);
|
||||||
|
|
||||||
|
|||||||
@@ -305,19 +305,20 @@ export function useBatchedSessionUpdates(
|
|||||||
// Session-level usage
|
// Session-level usage
|
||||||
const sessionUsageDelta = acc.usageDeltas.get(null);
|
const sessionUsageDelta = acc.usageDeltas.get(null);
|
||||||
if (sessionUsageDelta) {
|
if (sessionUsageDelta) {
|
||||||
const existing = updatedSession.usageStats;
|
const existing = updatedSession.usageStats;
|
||||||
updatedSession = {
|
updatedSession = {
|
||||||
...updatedSession,
|
...updatedSession,
|
||||||
usageStats: {
|
usageStats: {
|
||||||
inputTokens: (existing?.inputTokens || 0) + sessionUsageDelta.inputTokens,
|
inputTokens: (existing?.inputTokens || 0) + sessionUsageDelta.inputTokens,
|
||||||
outputTokens: (existing?.outputTokens || 0) + sessionUsageDelta.outputTokens,
|
outputTokens: (existing?.outputTokens || 0) + sessionUsageDelta.outputTokens,
|
||||||
cacheReadInputTokens: (existing?.cacheReadInputTokens || 0) + sessionUsageDelta.cacheReadInputTokens,
|
cacheReadInputTokens: (existing?.cacheReadInputTokens || 0) + sessionUsageDelta.cacheReadInputTokens,
|
||||||
cacheCreationInputTokens: (existing?.cacheCreationInputTokens || 0) + sessionUsageDelta.cacheCreationInputTokens,
|
cacheCreationInputTokens: (existing?.cacheCreationInputTokens || 0) + sessionUsageDelta.cacheCreationInputTokens,
|
||||||
totalCostUsd: (existing?.totalCostUsd || 0) + sessionUsageDelta.totalCostUsd,
|
totalCostUsd: (existing?.totalCostUsd || 0) + sessionUsageDelta.totalCostUsd,
|
||||||
contextWindow: sessionUsageDelta.contextWindow
|
reasoningTokens: (existing?.reasoningTokens || 0) + (sessionUsageDelta.reasoningTokens || 0),
|
||||||
|
contextWindow: sessionUsageDelta.contextWindow
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab-level usage
|
// Tab-level usage
|
||||||
if (updatedSession.aiTabs) {
|
if (updatedSession.aiTabs) {
|
||||||
@@ -335,8 +336,9 @@ export function useBatchedSessionUpdates(
|
|||||||
cacheReadInputTokens: tabUsageDelta.cacheReadInputTokens,
|
cacheReadInputTokens: tabUsageDelta.cacheReadInputTokens,
|
||||||
cacheCreationInputTokens: tabUsageDelta.cacheCreationInputTokens,
|
cacheCreationInputTokens: tabUsageDelta.cacheCreationInputTokens,
|
||||||
contextWindow: tabUsageDelta.contextWindow,
|
contextWindow: tabUsageDelta.contextWindow,
|
||||||
outputTokens: (existing?.outputTokens || 0) + tabUsageDelta.outputTokens,
|
outputTokens: tabUsageDelta.outputTokens, // Current (not accumulated)
|
||||||
totalCostUsd: (existing?.totalCostUsd || 0) + tabUsageDelta.totalCostUsd
|
totalCostUsd: (existing?.totalCostUsd || 0) + tabUsageDelta.totalCostUsd,
|
||||||
|
reasoningTokens: tabUsageDelta.reasoningTokens
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -490,8 +492,9 @@ export function useBatchedSessionUpdates(
|
|||||||
cacheReadInputTokens: usage.cacheReadInputTokens,
|
cacheReadInputTokens: usage.cacheReadInputTokens,
|
||||||
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
||||||
contextWindow: usage.contextWindow,
|
contextWindow: usage.contextWindow,
|
||||||
outputTokens: existing.outputTokens + usage.outputTokens,
|
outputTokens: usage.outputTokens,
|
||||||
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd
|
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd,
|
||||||
|
reasoningTokens: usage.reasoningTokens
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Session-level: all values are accumulated
|
// Session-level: all values are accumulated
|
||||||
@@ -501,6 +504,7 @@ export function useBatchedSessionUpdates(
|
|||||||
cacheReadInputTokens: existing.cacheReadInputTokens + usage.cacheReadInputTokens,
|
cacheReadInputTokens: existing.cacheReadInputTokens + usage.cacheReadInputTokens,
|
||||||
cacheCreationInputTokens: existing.cacheCreationInputTokens + usage.cacheCreationInputTokens,
|
cacheCreationInputTokens: existing.cacheCreationInputTokens + usage.cacheCreationInputTokens,
|
||||||
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd,
|
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd,
|
||||||
|
reasoningTokens: (existing.reasoningTokens || 0) + (usage.reasoningTokens || 0),
|
||||||
contextWindow: usage.contextWindow
|
contextWindow: usage.contextWindow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/types/vite-raw.d.ts
vendored
Normal file
4
src/types/vite-raw.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*?raw' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
13
tsconfig.lint.json
Normal file
13
tsconfig.lint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/renderer",
|
||||||
|
"src/shared",
|
||||||
|
"src/web",
|
||||||
|
"src/types"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -9,7 +9,14 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['./src/__tests__/setup.ts'],
|
setupFiles: ['./src/__tests__/setup.ts'],
|
||||||
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||||
exclude: ['node_modules', 'dist', 'release', 'src/__tests__/integration/**'],
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'dist',
|
||||||
|
'release',
|
||||||
|
'src/__tests__/integration/**',
|
||||||
|
'src/__tests__/e2e/**',
|
||||||
|
'src/__tests__/performance/**',
|
||||||
|
],
|
||||||
testTimeout: 10000,
|
testTimeout: 10000,
|
||||||
hookTimeout: 10000,
|
hookTimeout: 10000,
|
||||||
teardownTimeout: 5000,
|
teardownTimeout: 5000,
|
||||||
|
|||||||
Reference in New Issue
Block a user