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 .",
|
||||
"clean": "rm -rf dist release node_modules/.vite",
|
||||
"postinstall": "electron-rebuild -f -w node-pty",
|
||||
"lint": "tsc -p tsconfig.lint.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"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(
|
||||
<FilePreview
|
||||
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
|
||||
const container = screen.getByText('test.md').closest('[tabindex="0"]');
|
||||
fireEvent.keyDown(container!, { key: 'f', metaKey: true });
|
||||
@@ -2157,14 +2160,14 @@ describe('Search in markdown with highlighting', () => {
|
||||
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/);
|
||||
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(() => {
|
||||
// When searching in markdown, it shows raw content with highlights
|
||||
expect(screen.getByText('1/1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('react-markdown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,22 +233,6 @@ describe('TerminalOutput', () => {
|
||||
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', () => {
|
||||
const logs: LogEntry[] = [
|
||||
createLogEntry({ text: 'Delivered message', source: 'user', delivered: true }),
|
||||
|
||||
@@ -23,7 +23,7 @@ vi.mock('lucide-react', () => {
|
||||
return new Proxy({}, {
|
||||
get(_target, prop: string) {
|
||||
// Ignore internal properties
|
||||
if (prop === '__esModule' || prop === 'default' || typeof prop === 'symbol') {
|
||||
if (prop === '__esModule' || prop === 'default' || prop === 'then' || typeof prop === 'symbol') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,23 @@ vi.mock('lucide-react', () => {
|
||||
}
|
||||
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 { findPlaybookById } from '../services/playbooks';
|
||||
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 { formatRunEvent, formatError, formatInfo, formatWarning, RunEvent } from '../output/formatter';
|
||||
import { isSessionBusyWithCli, getCliActivityForSession } from '../../shared/cli-activity';
|
||||
@@ -94,17 +94,6 @@ export async function runPlaybook(playbookId: string, options: RunPlaybookOption
|
||||
const useJson = options.json;
|
||||
|
||||
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 playbook;
|
||||
|
||||
@@ -125,6 +114,37 @@ export async function runPlaybook(playbookId: string, options: RunPlaybookOption
|
||||
|
||||
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)
|
||||
let busyCheck = checkAgentBusy(agent.id, agent.name);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// 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 * as os from 'os';
|
||||
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';
|
||||
|
||||
// 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)
|
||||
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
|
||||
export interface AgentResult {
|
||||
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
|
||||
* 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Uses cached path from detectClaude() or falls back to default command
|
||||
@@ -156,10 +220,18 @@ export function getClaudeCommand(): string {
|
||||
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
|
||||
*/
|
||||
export async function spawnAgent(
|
||||
async function spawnClaudeAgent(
|
||||
cwd: string,
|
||||
prompt: 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
|
||||
*/
|
||||
|
||||
@@ -466,7 +466,7 @@ export async function* runPlaybook(
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -497,6 +497,7 @@ export async function* runPlaybook(
|
||||
if (result.success && result.agentSessionId) {
|
||||
// Request synopsis from the agent
|
||||
const synopsisResult = await spawnAgent(
|
||||
session.toolType,
|
||||
session.cwd,
|
||||
BATCH_SYNOPSIS_PROMPT,
|
||||
result.agentSessionId
|
||||
|
||||
@@ -19,6 +19,10 @@ Maestro is an Electron desktop application for managing multiple AI coding assis
|
||||
- **Git Branch:** {{GIT_BRANCH}}
|
||||
- **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
|
||||
|
||||
**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}}
|
||||
|
||||
## 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:
|
||||
- Start with higher confidence (60-70%) since you already have context
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
Through a brief, focused conversation:
|
||||
|
||||
@@ -178,32 +178,6 @@ export function AgentErrorModal({
|
||||
{new Date(error.timestamp).toLocaleTimeString()}
|
||||
</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 */}
|
||||
{hasJsonDetails && (
|
||||
<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]);
|
||||
|
||||
// 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(() => {
|
||||
if (!isMarkdown || markdownEditMode || !searchQuery.trim() || !markdownContainerRef.current) {
|
||||
if (!isMarkdown || markdownEditMode || !searchQuery.trim()) {
|
||||
if (isMarkdown && !markdownEditMode) {
|
||||
setTotalMatches(0);
|
||||
setCurrentMatchIndex(0);
|
||||
matchElementsRef.current = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const container = markdownContainerRef.current;
|
||||
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
|
||||
// Count matches in the raw content
|
||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
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
|
||||
textNodes.forEach(textNode => {
|
||||
const text = textNode.textContent || '';
|
||||
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' });
|
||||
setTotalMatches(count);
|
||||
if (count > 0 && currentMatchIndex >= count) {
|
||||
setCurrentMatchIndex(0);
|
||||
}
|
||||
|
||||
// Cleanup function to remove highlights
|
||||
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]);
|
||||
matchElementsRef.current = [];
|
||||
}, [searchQuery, file.content, isMarkdown, markdownEditMode, currentMatchIndex]);
|
||||
|
||||
const [copyNotificationMessage, setCopyNotificationMessage] = useState('');
|
||||
|
||||
@@ -851,47 +791,53 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
// Navigate to next search match
|
||||
const goToNextMatch = () => {
|
||||
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)
|
||||
const nextIndex = (currentMatchIndex + 1) % totalMatches;
|
||||
setCurrentMatchIndex(nextIndex);
|
||||
|
||||
// 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 code files, handle DOM-based highlighting
|
||||
const matches = matchElementsRef.current;
|
||||
if (matches.length > 0) {
|
||||
// Reset previous highlight
|
||||
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
|
||||
const goToPrevMatch = () => {
|
||||
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)
|
||||
const prevIndex = (currentMatchIndex - 1 + totalMatches) % totalMatches;
|
||||
setCurrentMatchIndex(prevIndex);
|
||||
|
||||
// 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 code files, handle DOM-based highlighting
|
||||
const matches = matchElementsRef.current;
|
||||
if (matches.length > 0) {
|
||||
// Reset previous highlight
|
||||
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
|
||||
@@ -901,25 +847,6 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!isMarkdown || !markdownEditMode || !searchQuery.trim() || !textareaRef.current) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
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 { MarkdownRenderer } from './MarkdownRenderer';
|
||||
import { stripMarkdown } from '../utils/textProcessing';
|
||||
@@ -198,7 +198,7 @@ export function GroupChatMessages({
|
||||
|
||||
{/* Message bubble */}
|
||||
<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={{
|
||||
backgroundColor: isUser
|
||||
? `color-mix(in srgb, ${theme.colors.accent} 20%, ${theme.colors.bgSidebar})`
|
||||
@@ -211,24 +211,6 @@ export function GroupChatMessages({
|
||||
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 */}
|
||||
{!isUser && (
|
||||
<div
|
||||
|
||||
@@ -123,9 +123,19 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
? hasCapability('supportsImageInputOnResume')
|
||||
: hasCapability('supportsImageInput');
|
||||
|
||||
// Check if we're in read-only mode (auto mode OR manual toggle - Claude will be in plan mode)
|
||||
const isAutoReadOnly = isAutoModeActive && session.inputMode === 'ai';
|
||||
const isReadOnlyMode = isAutoReadOnly || (tabReadOnlyMode && session.inputMode === 'ai');
|
||||
// Check if we're in read-only mode (manual toggle only - Claude will be in plan mode)
|
||||
// NOTE: Auto Run no longer forces read-only mode. Instead:
|
||||
// - 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
|
||||
const isTerminalMode = session.inputMode === 'terminal';
|
||||
@@ -539,8 +549,8 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
<div
|
||||
className="flex-1 relative border rounded-lg bg-opacity-50 flex flex-col"
|
||||
style={{
|
||||
borderColor: isReadOnlyMode ? theme.colors.warning : theme.colors.border,
|
||||
backgroundColor: isReadOnlyMode ? `${theme.colors.warning}15` : theme.colors.bgMain
|
||||
borderColor: showQueueingBorder ? theme.colors.warning : theme.colors.border,
|
||||
backgroundColor: showQueueingBorder ? `${theme.colors.warning}15` : theme.colors.bgMain
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
@@ -704,14 +714,11 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
</button>
|
||||
)}
|
||||
{/* 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') && (
|
||||
<button
|
||||
onClick={isAutoReadOnly ? undefined : onToggleTabReadOnlyMode}
|
||||
disabled={isAutoReadOnly}
|
||||
className={`flex items-center gap-1.5 text-[10px] px-2 py-1 rounded-full transition-all ${
|
||||
isAutoReadOnly ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
} ${
|
||||
onClick={onToggleTabReadOnlyMode}
|
||||
className={`flex items-center gap-1.5 text-[10px] px-2 py-1 rounded-full cursor-pointer transition-all ${
|
||||
isReadOnlyMode ? '' : 'opacity-40 hover:opacity-70'
|
||||
}`}
|
||||
style={{
|
||||
@@ -719,7 +726,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
color: isReadOnlyMode ? theme.colors.warning : theme.colors.textDim,
|
||||
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" />
|
||||
<span>Read-only</span>
|
||||
|
||||
@@ -314,7 +314,7 @@ const LogItemComponent = memo(({
|
||||
);
|
||||
})()}
|
||||
</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={{
|
||||
backgroundColor: isUserMessage
|
||||
? isAIMode
|
||||
@@ -327,23 +327,6 @@ const LogItemComponent = memo(({
|
||||
? theme.colors.accent + '40'
|
||||
: (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 */}
|
||||
{log.source !== 'user' && isTerminal && (
|
||||
<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 { parseSynopsis } from '../../shared/synopsis';
|
||||
|
||||
// Regex to count unchecked markdown checkboxes: - [ ] task
|
||||
const UNCHECKED_TASK_REGEX = /^[\s]*-\s*\[\s*\]\s*.+$/gm;
|
||||
// Regex to count unchecked markdown checkboxes: - [ ] task (also * [ ])
|
||||
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
|
||||
// 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
|
||||
const DEFAULT_BATCH_STATE: BatchRunState = {
|
||||
@@ -197,6 +200,14 @@ export function countUnfinishedTasks(content: string): number {
|
||||
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)
|
||||
* Converts all - [x] to - [ ] (case insensitive)
|
||||
@@ -708,6 +719,8 @@ ${docList}
|
||||
|
||||
// Read document and count tasks
|
||||
let { taskCount: remainingTasks, content: docContent } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
||||
let docCheckedCount = countCheckedTasks(docContent);
|
||||
let docTasksTotal = remainingTasks;
|
||||
|
||||
// Handle documents with no unchecked tasks
|
||||
if (remainingTasks === 0) {
|
||||
@@ -756,7 +769,7 @@ ${docList}
|
||||
[sessionId]: {
|
||||
...prev[sessionId],
|
||||
currentDocumentIndex: docIndex,
|
||||
currentDocTasksTotal: remainingTasks,
|
||||
currentDocTasksTotal: docTasksTotal,
|
||||
currentDocTasksCompleted: 0
|
||||
}
|
||||
}));
|
||||
@@ -820,8 +833,11 @@ ${docList}
|
||||
|
||||
// Re-read document to get updated task count and content
|
||||
const { taskCount: newRemainingTasks, content: contentAfterTask } = await readDocAndCountTasks(folderPath, docEntry.filename);
|
||||
// Calculate tasks completed - ensure it's never negative (Claude may have added tasks)
|
||||
const tasksCompletedThisRun = Math.max(0, remainingTasks - newRemainingTasks);
|
||||
const newCheckedCount = countCheckedTasks(contentAfterTask);
|
||||
// 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
|
||||
const documentUnchanged = contentBeforeTask === contentAfterTask;
|
||||
@@ -855,18 +871,30 @@ ${docList}
|
||||
}
|
||||
|
||||
// Update progress state
|
||||
updateBatchStateAndBroadcast(sessionId, prev => ({
|
||||
...prev,
|
||||
[sessionId]: {
|
||||
...prev[sessionId],
|
||||
currentDocTasksCompleted: docTasksCompleted,
|
||||
completedTasksAcrossAllDocs: totalCompletedTasks,
|
||||
// Legacy fields
|
||||
completedTasks: totalCompletedTasks,
|
||||
currentTaskIndex: totalCompletedTasks,
|
||||
sessionIds: [...(prev[sessionId]?.sessionIds || []), result.agentSessionId || '']
|
||||
}
|
||||
}));
|
||||
if (addedUncheckedTasks > 0) {
|
||||
docTasksTotal += addedUncheckedTasks;
|
||||
}
|
||||
|
||||
updateBatchStateAndBroadcast(sessionId, prev => {
|
||||
const prevState = prev[sessionId];
|
||||
const nextTotalAcrossAllDocs = Math.max(0, prevState.totalTasksAcrossAllDocs + addedUncheckedTasks);
|
||||
const nextTotalTasks = Math.max(0, prevState.totalTasks + addedUncheckedTasks);
|
||||
return {
|
||||
...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
|
||||
let shortSummary = `[${docEntry.filename}] Task completed`;
|
||||
@@ -977,6 +1005,7 @@ ${docList}
|
||||
break; // Break out of the inner while loop for this document
|
||||
}
|
||||
|
||||
docCheckedCount = newCheckedCount;
|
||||
remainingTasks = newRemainingTasks;
|
||||
console.log(`[BatchProcessor] Document ${docEntry.filename}: ${remainingTasks} tasks remaining`);
|
||||
|
||||
|
||||
@@ -305,19 +305,20 @@ export function useBatchedSessionUpdates(
|
||||
// Session-level usage
|
||||
const sessionUsageDelta = acc.usageDeltas.get(null);
|
||||
if (sessionUsageDelta) {
|
||||
const existing = updatedSession.usageStats;
|
||||
updatedSession = {
|
||||
...updatedSession,
|
||||
usageStats: {
|
||||
inputTokens: (existing?.inputTokens || 0) + sessionUsageDelta.inputTokens,
|
||||
outputTokens: (existing?.outputTokens || 0) + sessionUsageDelta.outputTokens,
|
||||
cacheReadInputTokens: (existing?.cacheReadInputTokens || 0) + sessionUsageDelta.cacheReadInputTokens,
|
||||
cacheCreationInputTokens: (existing?.cacheCreationInputTokens || 0) + sessionUsageDelta.cacheCreationInputTokens,
|
||||
totalCostUsd: (existing?.totalCostUsd || 0) + sessionUsageDelta.totalCostUsd,
|
||||
contextWindow: sessionUsageDelta.contextWindow
|
||||
const existing = updatedSession.usageStats;
|
||||
updatedSession = {
|
||||
...updatedSession,
|
||||
usageStats: {
|
||||
inputTokens: (existing?.inputTokens || 0) + sessionUsageDelta.inputTokens,
|
||||
outputTokens: (existing?.outputTokens || 0) + sessionUsageDelta.outputTokens,
|
||||
cacheReadInputTokens: (existing?.cacheReadInputTokens || 0) + sessionUsageDelta.cacheReadInputTokens,
|
||||
cacheCreationInputTokens: (existing?.cacheCreationInputTokens || 0) + sessionUsageDelta.cacheCreationInputTokens,
|
||||
totalCostUsd: (existing?.totalCostUsd || 0) + sessionUsageDelta.totalCostUsd,
|
||||
reasoningTokens: (existing?.reasoningTokens || 0) + (sessionUsageDelta.reasoningTokens || 0),
|
||||
contextWindow: sessionUsageDelta.contextWindow
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Tab-level usage
|
||||
if (updatedSession.aiTabs) {
|
||||
@@ -335,8 +336,9 @@ export function useBatchedSessionUpdates(
|
||||
cacheReadInputTokens: tabUsageDelta.cacheReadInputTokens,
|
||||
cacheCreationInputTokens: tabUsageDelta.cacheCreationInputTokens,
|
||||
contextWindow: tabUsageDelta.contextWindow,
|
||||
outputTokens: (existing?.outputTokens || 0) + tabUsageDelta.outputTokens,
|
||||
totalCostUsd: (existing?.totalCostUsd || 0) + tabUsageDelta.totalCostUsd
|
||||
outputTokens: tabUsageDelta.outputTokens, // Current (not accumulated)
|
||||
totalCostUsd: (existing?.totalCostUsd || 0) + tabUsageDelta.totalCostUsd,
|
||||
reasoningTokens: tabUsageDelta.reasoningTokens
|
||||
}
|
||||
};
|
||||
})
|
||||
@@ -490,8 +492,9 @@ export function useBatchedSessionUpdates(
|
||||
cacheReadInputTokens: usage.cacheReadInputTokens,
|
||||
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
||||
contextWindow: usage.contextWindow,
|
||||
outputTokens: existing.outputTokens + usage.outputTokens,
|
||||
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd
|
||||
outputTokens: usage.outputTokens,
|
||||
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd,
|
||||
reasoningTokens: usage.reasoningTokens
|
||||
});
|
||||
} else {
|
||||
// Session-level: all values are accumulated
|
||||
@@ -501,6 +504,7 @@ export function useBatchedSessionUpdates(
|
||||
cacheReadInputTokens: existing.cacheReadInputTokens + usage.cacheReadInputTokens,
|
||||
cacheCreationInputTokens: existing.cacheCreationInputTokens + usage.cacheCreationInputTokens,
|
||||
totalCostUsd: existing.totalCostUsd + usage.totalCostUsd,
|
||||
reasoningTokens: (existing.reasoningTokens || 0) + (usage.reasoningTokens || 0),
|
||||
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',
|
||||
setupFiles: ['./src/__tests__/setup.ts'],
|
||||
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,
|
||||
hookTimeout: 10000,
|
||||
teardownTimeout: 5000,
|
||||
|
||||
Reference in New Issue
Block a user