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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,28 @@ jobs:

- run: npm ci

- name: Typecheck
run: npm run typecheck
- name: Install Semgrep
run: |
python -m pip install --user --break-system-packages semgrep==1.145.0
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Lint
run: npm run lint
- name: Check
run: npm run check

- name: Format check
run: npm run format:check
- name: Test
run: npm test

- name: Dead-code lint
run: npm run lint:dead

- name: Architecture lint
run: npm run lint:arch

- name: Security lint
run: npm run lint:security

- name: Security rule fixtures
run: npm run test:security-rules

- name: OpenSpec
run: npx openspec validate --all --strict
2 changes: 0 additions & 2 deletions .npmrc

This file was deleted.

4 changes: 0 additions & 4 deletions electron/ipc/ask-code-minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ export function setMinimaxApiKey(key: string): void {
storedApiKey = key.trim();
}

export function getMinimaxApiKey(): string {
return storedApiKey;
}

export function askAboutCodeMinimax(win: BrowserWindow, args: MinimaxAskCodeRequest): void {
const { requestId, channelId, prompt } = args;
const apiKey = storedApiKey;
Expand Down
4 changes: 1 addition & 3 deletions electron/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,7 @@ export function registerAllHandlers(win: BrowserWindow): void {
if (basename !== args.filename) throw new Error('Invalid filename');
if (!basename.startsWith('arena-') || !basename.endsWith('.json'))
throw new Error('Arena files must be arena-*.json');
const tmpPath = filePath + '.tmp';
fs.writeFileSync(tmpPath, args.json, 'utf-8');
fs.renameSync(tmpPath, filePath);
atomicWriteFileSync(filePath, args.json);
});

ipcMain.handle(IPC.LoadArenaData, (_e, args) => {
Expand Down
4 changes: 0 additions & 4 deletions electron/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ const swallowEpipe = (err: NodeJS.ErrnoException): void => {
process.stdout.on('error', swallowEpipe);
process.stderr.on('error', swallowEpipe);

export function setMinLevel(level: LogLevel): void {
minLevel = level;
}

export function getMinLevel(): LogLevel {
return minLevel;
}
Expand Down
43 changes: 43 additions & 0 deletions electron/mcp/coordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3012,6 +3012,10 @@ describe('Coordinator waitForSignalDone — requestId replay after transport fai
coordinator.registerCoordinator('coord-1', 'proj-1');
});

afterEach(() => {
vi.useRealTimers();
});

it('same requestId returns cached result after signal is already consumed', async () => {
await coordinator.createTask({ name: 'test', prompt: 'do', coordinatorTaskId: 'coord-1' });
const task = coordinator.getTask('task-1');
Expand Down Expand Up @@ -3076,6 +3080,45 @@ describe('Coordinator waitForSignalDone — requestId replay after transport fai
const result2 = await coordinator.waitForSignalDone('coord-2', 50, requestId);
expect(result2.timedOut).toBe(true);
});

it('same requestId returns cached timeout result immediately', async () => {
vi.useFakeTimers();
await coordinator.createTask({ name: 'test', prompt: 'do', coordinatorTaskId: 'coord-1' });

const requestId = 'timeout-replay-id';
const firstWait = coordinator.waitForSignalDone('coord-1', 500, requestId);
vi.advanceTimersByTime(500);
const firstResult = await firstWait;

expect(firstResult).toEqual({ remaining: 1, timedOut: true });

const replay = coordinator.waitForSignalDone('coord-1', 500, requestId);
let replayResult: Awaited<typeof replay> | undefined;
replay.then((result) => {
replayResult = result;
});
await Promise.resolve();

expect(replayResult).toEqual(firstResult);
});

it('same requestId replays cached timeout after coordinator deregisters', async () => {
vi.useFakeTimers();
await coordinator.createTask({ name: 'test', prompt: 'do', coordinatorTaskId: 'coord-1' });

const requestId = 'timeout-replay-after-deregister-id';
const firstWait = coordinator.waitForSignalDone('coord-1', 500, requestId);
vi.advanceTimersByTime(500);
const firstResult = await firstWait;

expect(firstResult).toEqual({ remaining: 1, timedOut: true });

await coordinator.deregisterCoordinator('coord-1');

await expect(coordinator.waitForSignalDone('coord-1', 500, requestId)).resolves.toEqual(
firstResult,
);
});
});

// ─── removePreambleBlock unit tests ──────────────────────────────────────────
Expand Down
8 changes: 4 additions & 4 deletions electron/mcp/coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1628,16 +1628,16 @@ export class Coordinator {
timeoutMs = DEFAULT_WAIT_TIMEOUT_MS,
requestId?: string,
): Promise<WaitForSignalDoneResult> {
if (!this.coordinators.has(coordinatorTaskId)) {
return Promise.reject(new Error(`Coordinator not found: ${coordinatorTaskId}`));
}
// Replay the cached result if this requestId already delivered — handles retry
// after the HTTP response was lost before the client received it.
// Key includes coordinatorTaskId to prevent cross-coordinator replay.
if (requestId) {
const cached = this.recentlyDelivered.get(coordinatorTaskId, requestId);
if (cached) return Promise.resolve(cached);
}
if (!this.coordinators.has(coordinatorTaskId)) {
return Promise.reject(new Error(`Coordinator not found: ${coordinatorTaskId}`));
}
// Return immediately if there's an unconsumed signal
for (const task of this.tasks.values()) {
if (
Expand Down Expand Up @@ -1696,7 +1696,7 @@ export class Coordinator {
activeWaitCount: this.activeSignalWaitCounts.get(coordinatorTaskId) ?? 0,
});
const remaining = this.countRemaining(coordinatorTaskId);
resolve({ remaining, timedOut: true });
wrapped({ remaining, timedOut: true });
}, timeoutMs);

let resolvers = this.anySignalResolvers.get(coordinatorTaskId);
Expand Down
8 changes: 4 additions & 4 deletions electron/mcp/preamble.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { randomUUID } from 'crypto';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs';
import { readFileSync, existsSync, unlinkSync } from 'fs';
import { readFile as fsReadFile, unlink as fsUnlink } from 'fs/promises';
import { atomicWriteFile } from './atomic.js';
import { atomicWriteFile, atomicWriteFileSync } from './atomic.js';
import { join } from 'path';
import os from 'os';

Expand Down Expand Up @@ -127,8 +127,8 @@ export async function buildNormalizedPreambleFileDiff(
const tmpBase = join(os.tmpdir(), `parallel-code-base-${id}`);
const tmpNorm = join(os.tmpdir(), `parallel-code-norm-${id}`);
try {
writeFileSync(tmpBase, baseContent);
writeFileSync(tmpNorm, normalizedContent);
atomicWriteFileSync(tmpBase, baseContent);
atomicWriteFileSync(tmpNorm, normalizedContent);
let diffOut = '';
try {
const { stdout } = await execAsync('git', ['diff', '--no-index', '-U3', tmpBase, tmpNorm]);
Expand Down
43 changes: 0 additions & 43 deletions electron/mcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,49 +85,6 @@ export interface CoordinatorState {
writtenMcpParallelCode?: unknown;
}

// --- MCP tool input schemas ---

export interface CreateTaskInput {
name: string;
prompt?: string;
projectId?: string;
}

export type ListTasksInput = Record<string, never>;

export interface GetTaskStatusInput {
taskId: string;
}

export interface SendPromptInput {
taskId: string;
prompt: string;
}

export interface WaitForIdleInput {
taskId: string;
timeoutMs?: number;
}

export interface GetTaskDiffInput {
taskId: string;
}

export interface GetTaskOutputInput {
taskId: string;
}

export interface MergeTaskInput {
taskId: string;
squash?: boolean;
message?: string;
cleanup?: boolean;
}

export interface CloseTaskInput {
taskId: string;
}

// --- API request/response types ---

export interface ApiTaskSummary {
Expand Down
21 changes: 1 addition & 20 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,9 @@ const config: KnipConfig = {
'electron/main.ts',
'electron/preload.cjs',
'electron/mcp/server.ts', // added in coordinator-2-mcp-backend; listed here so knip tracks it from the start
'src/main.tsx',
'src/remote/main.tsx',
],
project: ['electron/**/*.ts', 'src/**/*.{ts,tsx}'],
ignore: [
'dist/**',
'dist-electron/**',
'dist-remote/**',
'release/**',
'scripts/**',
// Vite/Electron configs are entry points picked up by their respective tools
'electron/vite.config.electron.ts',
'electron/vite.config.electron.test.ts',
'electron/shims/**',
],
ignoreDependencies: [
// Peer dependencies and indirect runtime deps
'electron',
'electron-builder',
'concurrently',
'wait-on',
],
ignoreBinaries: ['gitleaks'],
// Test files are allowed to have unused exports (test helpers, fixtures)
ignoreExportsUsedInFile: true,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@
`saveAppState()` output.
- [x] Update unit tests in `src/lib/custom-theme.test.ts` and
`src/store/appearance-mode.test.ts`.
- [ ] Validate with `npm run typecheck`, `npm test`, and
- [x] Validate with `npm run typecheck`, `npm test`, and
`openspec validate --all --strict`.
16 changes: 10 additions & 6 deletions openspec/specs/custom-themes/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ custom theme overrides color variables.

### Requirement: Terminal readability for light custom themes

When a custom theme's `terminalBackground` has luminance > 0.5, the terminal
emulator SHALL use a dark foreground color and a GitHub-light-compatible ANSI
palette so that colored output remains legible.
The terminal emulator SHALL use a dark foreground color and a
GitHub-light-compatible ANSI palette when a custom theme's
`terminalBackground` has luminance > 0.5 so colored output remains legible.

#### Scenario: Light background gets dark foreground

Expand All @@ -141,7 +141,11 @@ palette so that colored output remains legible.
The theme dialog SHALL report contrast warnings for pairs that fail WCAG AA
thresholds so users can correct them before saving.

#### Contrast pairs checked
#### Scenario: Contrast pairs are checked

- **GIVEN** the theme dialog validates a custom theme
- **WHEN** it computes WCAG contrast warnings
- **THEN** it checks these foreground/background pairs:

| Foreground | Background | Required ratio |
| --------------- | --------------- | -------------- |
Expand All @@ -150,5 +154,5 @@ thresholds so users can correct them before saving.
| `--fg` | `--bg-selected` | 4.5 : 1 |
| `--accent-text` | `--accent` | 4.5 : 1 |

Translucent backgrounds SHALL be composited over `--bg-elevated` before the
ratio is computed to avoid false positives.
- **AND** translucent backgrounds are composited over `--bg-elevated` before the
ratio is computed to avoid false positives
Loading