From 583bf65e51e40055a0266e4ea032cf8582cc1b62 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 29 May 2026 13:05:01 +0200 Subject: [PATCH 1/2] fix(terminal): repaint to recover from WebGL atlas corruption (#121) macOS-only automatic recovery (foreground + window-refocus repaints) plus a cross-platform manual redraw shortcut (Cmd/Ctrl+Shift+L). Clears the glyph texture atlas and forces a full refresh; the buffer is intact, only the GPU glyph cache corrupts. --- src/App.tsx | 12 +++++++++++- src/components/TerminalView.tsx | 24 +++++++++++++++++++++++- src/lib/keybindings/defaults.ts | 16 ++++++++++++++++ src/lib/terminalFitManager.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index df22c92d..04d5166a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,6 +81,7 @@ import { osIsDark } from './lib/os-appearance'; import { applyAppearanceMode, markCustomThemesReady, loadCustomThemes } from './store/store'; import { isMac, mod } from './lib/platform'; import { createCtrlWheelZoomHandler } from './lib/wheelZoom'; +import { redrawAllTerminals } from './lib/terminalFitManager'; import { ArenaOverlay } from './arena/ArenaOverlay'; import { startDesktopNotificationWatcher } from './store/desktopNotifications'; import { startPrChecksSubscription } from './store/pr-checks'; @@ -335,7 +336,15 @@ function App() { void (async () => { try { unlistenFocusChanged = await appWindow.onFocusChanged((event) => { - setWindowFocused(Boolean(event.payload)); + const focused = Boolean(event.payload); + setWindowFocused(focused); + // The compositor can throttle the window's WebGL surface while it's + // backgrounded, leaving a corrupt glyph atlas (issue #121). Repaint + // every terminal on refocus so the corruption clears reliably. + // macOS-only: the corruption has never been reported on Linux, so + // Linux users don't pay the per-refocus repaint. The manual + // redrawTerminals shortcut stays cross-platform as an escape hatch. + if (focused && isMac) redrawAllTerminals(); }); } catch { unlistenFocusChanged = null; @@ -672,6 +681,7 @@ function App() { } }, resetZoom: () => resetGlobalScale(), + redrawTerminals: () => redrawAllTerminals(), }; const cleanupZoomShortcuts = registerZoomShortcuts({ diff --git a/src/components/TerminalView.tsx b/src/components/TerminalView.tsx index ea1ebc0a..1850c7be 100644 --- a/src/components/TerminalView.tsx +++ b/src/components/TerminalView.tsx @@ -14,7 +14,12 @@ import { resolvedBindings } from '../store/keybindings'; import { matchesKeyEvent } from '../lib/keybindings'; import { store, setTaskLastInputAt, retryTaskMcpStartup } from '../store/store'; import { warn as logWarn } from '../lib/log'; -import { registerTerminal, unregisterTerminal, markDirty } from '../lib/terminalFitManager'; +import { + registerTerminal, + unregisterTerminal, + markDirty, + redrawTerminal, +} from '../lib/terminalFitManager'; import { dataTransferToShellArgs, escapePath } from '../lib/terminalDrop'; import { cleanCopiedTerminalText } from '../lib/copy-text'; import { computeDisableStdin } from '../lib/terminalDisableStdin'; @@ -619,6 +624,23 @@ export function TerminalView(props: TerminalViewProps) { term.options.cursorBlink = props.isFocused === true; }); + // Force a clean repaint when this pane returns to the foreground. In focus + // mode inactive panes are hidden with visibility:hidden (so the + // IntersectionObserver in terminalFitManager never fires), and a WebGL + // terminal whose GPU surface was throttled while backgrounded can come + // back with a corrupt glyph atlas. Redraw on the hidden→visible edge so + // foregrounding reliably clears the corruption rather than "sometimes". + // macOS-only (issue #121): never reported on Linux, so Linux skips the + // reactive subscription and the repaints entirely. + if (isMac) { + let prevVisible: boolean | undefined; + createEffect(() => { + const visible = !store.focusMode || store.activeTaskId === taskId; + if (prevVisible === false && visible) redrawTerminal(agentId); + prevVisible = visible; + }); + } + // Load WebGL addon for all terminals. On context loss (e.g. too many // WebGL contexts), the terminal gracefully falls back to the DOM renderer. try { diff --git a/src/lib/keybindings/defaults.ts b/src/lib/keybindings/defaults.ts index edf7053a..c1b13aef 100644 --- a/src/lib/keybindings/defaults.ts +++ b/src/lib/keybindings/defaults.ts @@ -357,6 +357,22 @@ export const DEFAULT_BINDINGS: KeyBinding[] = [ action: 'resetZoom', global: true, }, + { + // Manual escape hatch for the WebGL rendering corruption in issue #121. + // The automatic recovery (refocus/foreground repaints) is macOS-only, but + // this stays cross-platform: it is inert until pressed and works as a + // general "repaint if something looks off" command on any GPU. + // Not Cmd/Ctrl+Shift+R — that is Electron's default Force Reload accelerator. + id: 'app.redraw-terminals', + layer: 'app', + category: 'App', + description: 'Redraw terminals (fix rendering glitches)', + platform: 'both', + key: 'L', + modifiers: { cmdOrCtrl: true, shift: true }, + action: 'redrawTerminals', + global: true, + }, // ------------------------------------------------------------------------- // Terminal layer — Copy / Paste diff --git a/src/lib/terminalFitManager.ts b/src/lib/terminalFitManager.ts index 95cd6bb9..e3327da8 100644 --- a/src/lib/terminalFitManager.ts +++ b/src/lib/terminalFitManager.ts @@ -113,3 +113,30 @@ export function markDirty(id: string): void { scheduleFlush(); } } + +/** + * Force a clean repaint of a terminal: discard the renderer's glyph texture + * atlas, then mark every row dirty so the next frame re-rasterizes from a + * fresh atlas. Recovers from xterm WebGL atlas corruption (issue #121) where + * glyphs render from stale/wrong atlas cells — the buffer is intact, only the + * GPU glyph cache is bad, so a plain refresh() would just redraw the same + * garbage. clearTextureAtlas() is a safe no-op under the DOM renderer. + */ +function redraw(term: Terminal): void { + try { + term.clearTextureAtlas(); + term.refresh(0, term.rows - 1); + } catch { + // The terminal may be mid-dispose (e.g. a window-focus event racing + // teardown). A best-effort cosmetic redraw must never crash the app. + } +} + +export function redrawTerminal(id: string): void { + const entry = entries.get(id); + if (entry) redraw(entry.term); +} + +export function redrawAllTerminals(): void { + for (const [, entry] of entries) redraw(entry.term); +} From a42aa98340fcb1b0fbf20bd55f1cea03d92f723f Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sat, 30 May 2026 19:17:31 +0200 Subject: [PATCH 2/2] fix(coordinator): repair quality gates and signal replay --- .github/workflows/ci.yml | 29 +- .npmrc | 2 - electron/ipc/ask-code-minimax.ts | 4 - electron/ipc/register.ts | 4 +- electron/log.ts | 4 - electron/mcp/coordinator.test.ts | 43 + electron/mcp/coordinator.ts | 8 +- electron/mcp/preamble.ts | 8 +- electron/mcp/types.ts | 43 - knip.config.ts | 21 +- .../2026-05-30-custom-themes}/proposal.md | 0 .../2026-05-30-custom-themes}/tasks.md | 2 +- openspec/specs/custom-themes/spec.md | 16 +- package-lock.json | 967 ++++++++++++++++-- package.json | 4 +- scripts/test-semgrep-filesystem-safety.mjs | 17 +- src/arena/store.ts | 4 - src/components/MonacoDiffEditor.tsx | 91 -- src/ipc/types.ts | 19 - src/lib/diff-parser.ts | 4 - src/lib/log.ts | 8 - src/remote/auth.ts | 11 - src/remote/ws.ts | 10 - src/store/active-task.ts | 46 + src/store/agents.ts | 10 - src/store/focus.test.ts | 2 +- src/store/focus.ts | 8 +- src/store/navigation.ts | 57 +- src/store/persistence.ts | 6 +- src/store/project-color.ts | 6 + src/store/project-removal.ts | 29 + src/store/projects.ts | 35 +- src/store/store.ts | 18 +- src/store/task-uncollapse.ts | 66 ++ src/store/tasks.ts | 67 -- src/store/terminal-counter.ts | 29 + src/store/terminals.ts | 22 +- src/store/ui.ts | 14 - 38 files changed, 1204 insertions(+), 530 deletions(-) delete mode 100644 .npmrc rename openspec/changes/{custom-themes => archive/2026-05-30-custom-themes}/proposal.md (100%) rename openspec/changes/{custom-themes => archive/2026-05-30-custom-themes}/tasks.md (97%) delete mode 100644 src/components/MonacoDiffEditor.tsx delete mode 100644 src/lib/diff-parser.ts create mode 100644 src/store/active-task.ts create mode 100644 src/store/project-color.ts create mode 100644 src/store/project-removal.ts create mode 100644 src/store/task-uncollapse.ts create mode 100644 src/store/terminal-counter.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 329aca0b..2fbd8294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index efc037aa..00000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -onlyBuiltDependencies: - - esbuild diff --git a/electron/ipc/ask-code-minimax.ts b/electron/ipc/ask-code-minimax.ts index 30a6b282..bde58b3c 100644 --- a/electron/ipc/ask-code-minimax.ts +++ b/electron/ipc/ask-code-minimax.ts @@ -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; diff --git a/electron/ipc/register.ts b/electron/ipc/register.ts index f561f4a2..616ce21b 100644 --- a/electron/ipc/register.ts +++ b/electron/ipc/register.ts @@ -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) => { diff --git a/electron/log.ts b/electron/log.ts index 107b24b5..72af0e88 100644 --- a/electron/log.ts +++ b/electron/log.ts @@ -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; } diff --git a/electron/mcp/coordinator.test.ts b/electron/mcp/coordinator.test.ts index 7c353f74..17621aaf 100644 --- a/electron/mcp/coordinator.test.ts +++ b/electron/mcp/coordinator.test.ts @@ -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'); @@ -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 | 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 ────────────────────────────────────────── diff --git a/electron/mcp/coordinator.ts b/electron/mcp/coordinator.ts index 8d8fcab5..a98683c2 100644 --- a/electron/mcp/coordinator.ts +++ b/electron/mcp/coordinator.ts @@ -1628,9 +1628,6 @@ export class Coordinator { timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, requestId?: string, ): Promise { - 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. @@ -1638,6 +1635,9 @@ export class Coordinator { 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 ( @@ -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); diff --git a/electron/mcp/preamble.ts b/electron/mcp/preamble.ts index 12d2d7c3..f22a42d9 100644 --- a/electron/mcp/preamble.ts +++ b/electron/mcp/preamble.ts @@ -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'; @@ -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]); diff --git a/electron/mcp/types.ts b/electron/mcp/types.ts index 681e9813..d4647b9e 100644 --- a/electron/mcp/types.ts +++ b/electron/mcp/types.ts @@ -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; - -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 { diff --git a/knip.config.ts b/knip.config.ts index a9639eb2..f72df71b 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -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, }; diff --git a/openspec/changes/custom-themes/proposal.md b/openspec/changes/archive/2026-05-30-custom-themes/proposal.md similarity index 100% rename from openspec/changes/custom-themes/proposal.md rename to openspec/changes/archive/2026-05-30-custom-themes/proposal.md diff --git a/openspec/changes/custom-themes/tasks.md b/openspec/changes/archive/2026-05-30-custom-themes/tasks.md similarity index 97% rename from openspec/changes/custom-themes/tasks.md rename to openspec/changes/archive/2026-05-30-custom-themes/tasks.md index 7fab39b0..0bff8075 100644 --- a/openspec/changes/custom-themes/tasks.md +++ b/openspec/changes/archive/2026-05-30-custom-themes/tasks.md @@ -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`. diff --git a/openspec/specs/custom-themes/spec.md b/openspec/specs/custom-themes/spec.md index 1781ed55..f657724b 100644 --- a/openspec/specs/custom-themes/spec.md +++ b/openspec/specs/custom-themes/spec.md @@ -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 @@ -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 | | --------------- | --------------- | -------------- | @@ -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 diff --git a/package-lock.json b/package-lock.json index 7f70ffaf..4ced623a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.3", - "@types/dompurify": "^3.0.5", + "@fission-ai/openspec": "^1.3.1", "@types/node": "^25.3.0", "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", @@ -1509,6 +1509,250 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fission-ai/openspec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@fission-ai/openspec/-/openspec-1.3.1.tgz", + "integrity": "sha512-QnbJfq/lUNCRY+TTXo87fuIpGCCaOYt280tmbuI112B/1vF0feIneK0/qhoTZNslRDhwwg1YcYDX0suxq2h6tw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.2", + "@inquirer/prompts": "^7.8.0", + "chalk": "^5.5.0", + "commander": "^14.0.0", + "fast-glob": "^3.3.3", + "ora": "^8.2.0", + "posthog-node": "^5.20.0", + "yaml": "^2.8.2", + "zod": "^4.0.17" + }, + "bin": { + "openspec": "bin/openspec.js" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@fission-ai/openspec/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@fission-ai/openspec/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@fission-ai/openspec/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@fission-ai/openspec/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fission-ai/openspec/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fission-ai/openspec/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fission-ai/openspec/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@fontsource/inter": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", @@ -1611,73 +1855,468 @@ "hono": "^4" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, "engines": { - "node": ">=18.18.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": ">=18.18.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=12.22" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=18.18" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", - "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.1.0", - "@iconify/types": "^2.0.0", - "mlly": "^1.8.0" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@isaacs/cliui": { @@ -2014,6 +2653,44 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -2710,6 +3387,16 @@ "node": ">=14" } }, + "node_modules/@posthog/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.10.0.tgz", + "integrity": "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -3543,16 +4230,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dompurify": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3671,8 +4348,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -5288,6 +5965,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -5365,6 +6049,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -7592,6 +8286,36 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7622,6 +8346,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fd-package-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", @@ -9845,6 +10579,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/mermaid": { "version": "11.15.0", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", @@ -10313,6 +11057,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nano-spawn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", @@ -11054,6 +11808,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/posthog-node": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.2.tgz", + "integrity": "sha512-Jehlu0KguL1LLyUczCt86OtA5INmeStK3zcgbv1BSyMcNxs0HP3GQogBrYhwhqHsk6JopiFFVpJyZEoXOUMhGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@posthog/core": "1.10.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", @@ -11374,6 +12141,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -11630,6 +12418,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -11750,6 +12549,30 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -12323,6 +13146,19 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13633,6 +14469,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index b7ba434e..258740b3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "lint:fix": "eslint . --fix", "lint:dead": "knip", "lint:arch": "depcruise --config .dependency-cruiser.cjs src electron", - "lint:security": "command -v semgrep >/dev/null 2>&1 && semgrep scan --config .semgrep/ --severity WARNING --error || (echo 'semgrep not installed (brew install semgrep or pip install semgrep)' >&2; exit 1)", + "lint:security": "sh -c 'command -v semgrep >/dev/null 2>&1 || { echo \"semgrep not installed (brew install semgrep or pip install semgrep)\" >&2; exit 1; }; semgrep scan --config .semgrep/ --severity WARNING --error'", "test:security-rules": "node scripts/test-semgrep-filesystem-safety.mjs", "lint:secrets": "command -v gitleaks >/dev/null 2>&1 && gitleaks detect --config .gitleaks.toml || (echo 'gitleaks not installed (brew install gitleaks)' >&2; exit 1)", "format": "prettier --write .", @@ -59,7 +59,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.3", - "@types/dompurify": "^3.0.5", + "@fission-ai/openspec": "^1.3.1", "@types/node": "^25.3.0", "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", diff --git a/scripts/test-semgrep-filesystem-safety.mjs b/scripts/test-semgrep-filesystem-safety.mjs index 3249f258..4bb41e0b 100644 --- a/scripts/test-semgrep-filesystem-safety.mjs +++ b/scripts/test-semgrep-filesystem-safety.mjs @@ -1,11 +1,24 @@ #!/usr/bin/env node /* global console, process */ -import { execFileSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +const semgrepCheck = spawnSync('semgrep', ['--version'], { stdio: 'ignore' }); +if (semgrepCheck.error?.code === 'ENOENT') { + console.error( + 'semgrep not installed; cannot run filesystem-safety rule fixture test ' + + '(brew install semgrep or pip install semgrep)', + ); + process.exit(1); +} +if (semgrepCheck.status !== 0) { + console.error('semgrep is installed but failed to run'); + process.exit(semgrepCheck.status ?? 1); +} + const tmp = mkdtempSync(join(tmpdir(), 'parallel-code-semgrep-')); try { @@ -37,7 +50,7 @@ try { const stdout = execFileSync( 'semgrep', - ['scan', '--config', '.semgrep/filesystem-safety.yml', '--json', tmp], + ['scan', '--config', '.semgrep/filesystem-safety.yml', '--json', '--no-git-ignore', tmp], { cwd: process.cwd(), encoding: 'utf8', diff --git a/src/arena/store.ts b/src/arena/store.ts index ee77db87..e78cdb2c 100644 --- a/src/arena/store.ts +++ b/src/arena/store.ts @@ -144,10 +144,6 @@ export function addMatchToHistory(match: ArenaMatch): void { setState('history', (prev) => [match, ...prev]); } -export function setSelectedHistoryMatch(match: ArenaMatch | null): void { - setState('selectedHistoryMatch', match); -} - export function updateHistoryRating( matchId: string, competitorIndex: number, diff --git a/src/components/MonacoDiffEditor.tsx b/src/components/MonacoDiffEditor.tsx deleted file mode 100644 index 6ccafcb0..00000000 --- a/src/components/MonacoDiffEditor.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { onMount, onCleanup, createEffect } from 'solid-js'; -import * as monaco from 'monaco-editor'; -import { store } from '../store/core'; -import { monacoThemeName } from '../lib/monaco-theme'; - -interface MonacoDiffEditorProps { - oldContent: string; - newContent: string; - language: string; - sideBySide: boolean; -} - -export function MonacoDiffEditor(props: MonacoDiffEditorProps) { - let containerRef!: HTMLDivElement; - let editor: monaco.editor.IStandaloneDiffEditor | undefined; - let originalModel: monaco.editor.ITextModel | undefined; - let modifiedModel: monaco.editor.ITextModel | undefined; - - onMount(() => { - editor = monaco.editor.createDiffEditor(containerRef, { - automaticLayout: true, - readOnly: true, - renderSideBySide: props.sideBySide, - theme: monacoThemeName(store.themePreset), - fontSize: 12, - fontFamily: "'JetBrains Mono', monospace", - minimap: { enabled: false }, - scrollBeyondLastLine: false, - renderOverviewRuler: false, - stickyScroll: { enabled: false }, - hideUnchangedRegions: { enabled: true }, - }); - - originalModel = monaco.editor.createModel(props.oldContent, props.language); - modifiedModel = monaco.editor.createModel(props.newContent, props.language); - editor.setModel({ original: originalModel, modified: modifiedModel }); - - editor.onDidUpdateDiff(() => { - const changes = editor?.getLineChanges(); - if (changes && changes.length > 0) { - const line = changes[0].modifiedStartLineNumber; - editor?.getModifiedEditor().revealLineInCenter(line); - } - }); - - // Make the entire hidden-lines bar clickable (Monaco only wires a tiny icon by default) - containerRef.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - const center = target.closest('.diff-hidden-lines .center'); - if (!center) return; - const link = center.querySelector('a[role="button"]'); - if (link && !link.contains(target)) link.click(); - }); - }); - - createEffect(() => { - const lang = props.language; - if (originalModel) monaco.editor.setModelLanguage(originalModel, lang); - if (modifiedModel) monaco.editor.setModelLanguage(modifiedModel, lang); - }); - - createEffect(() => { - const value = props.oldContent; - if (originalModel && originalModel.getValue() !== value) { - originalModel.setValue(value); - } - }); - - createEffect(() => { - const value = props.newContent; - if (modifiedModel && modifiedModel.getValue() !== value) { - modifiedModel.setValue(value); - } - }); - - createEffect(() => { - editor?.updateOptions({ renderSideBySide: props.sideBySide }); - }); - - createEffect(() => { - monaco.editor.setTheme(monacoThemeName(store.themePreset)); - }); - - onCleanup(() => { - editor?.dispose(); - originalModel?.dispose(); - modifiedModel?.dispose(); - }); - - return
; -} diff --git a/src/ipc/types.ts b/src/ipc/types.ts index ff259c04..15ffd44d 100644 --- a/src/ipc/types.ts +++ b/src/ipc/types.ts @@ -27,15 +27,6 @@ export interface CreateTaskResult { worktree_path: string; } -export interface TaskInfo { - id: string; - name: string; - branch_name: string; - worktree_path: string; - agent_ids: string[]; - status: 'Active' | 'Closed'; -} - export interface ChangedFile { path: string; lines_added: number; @@ -125,16 +116,6 @@ export interface PrChecksUpdatePayload { cleared: boolean; } -export interface StartPrChecksWatcherArgs { - taskId: string; - prUrl: string; - taskName: string; -} - -export interface StopPrChecksWatcherArgs { - taskId: string; -} - // The main-process updater owns these types; re-exported so the renderer // shares one source of truth and cannot drift from it. export type { UpdatePhase, UpdateStatus } from '../../electron/ipc/updater'; diff --git a/src/lib/diff-parser.ts b/src/lib/diff-parser.ts deleted file mode 100644 index 685796f6..00000000 --- a/src/lib/diff-parser.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** Check if diff output indicates a binary file. */ -export function isBinaryDiff(raw: string): boolean { - return raw.includes('Binary files') && raw.includes('differ'); -} diff --git a/src/lib/log.ts b/src/lib/log.ts index 9bd70caf..5c56df8d 100644 --- a/src/lib/log.ts +++ b/src/lib/log.ts @@ -48,14 +48,6 @@ export function setVerbose(value: boolean): void { minLevel = value ? 'debug' : buildDefault; } -export function getMinLevel(): LogLevel { - return minLevel; -} - -export function isVerbose(): boolean { - return verbose; -} - export function debug(category: string, msg: string, ctx?: LogContext): void { emit('debug', category, msg, ctx); } diff --git a/src/remote/auth.ts b/src/remote/auth.ts index 52f1efc5..e34c4865 100644 --- a/src/remote/auth.ts +++ b/src/remote/auth.ts @@ -25,14 +25,3 @@ export function getToken(): string | null { export function clearToken(): void { localStorage.removeItem(TOKEN_KEY); } - -/** Build an authenticated URL for API requests. */ -export function apiUrl(path: string): string { - return `${window.location.origin}${path}`; -} - -/** Build headers with auth token. */ -export function authHeaders(): Record { - const token = getToken(); - return token ? { Authorization: `Bearer ${token}` } : {}; -} diff --git a/src/remote/ws.ts b/src/remote/ws.ts index 037ed6d6..e060a529 100644 --- a/src/remote/ws.ts +++ b/src/remote/ws.ts @@ -104,16 +104,6 @@ export function connect(): void { }; } -export function disconnect(): void { - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - ws?.close(); - ws = null; - setStatus('disconnected'); -} - export function send(msg: Record): void { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(msg)); diff --git a/src/store/active-task.ts b/src/store/active-task.ts new file mode 100644 index 00000000..9a483fdb --- /dev/null +++ b/src/store/active-task.ts @@ -0,0 +1,46 @@ +import { store, setStore } from './core'; + +const AI_TERMINAL_PREFIX = 'ai-terminal:'; + +function focusedAgentIdForTask(taskId: string, agentIds: string[]): string | null { + const panel = store.focusedPanel[taskId]; + if (!panel?.startsWith(AI_TERMINAL_PREFIX)) return null; + const agentId = panel.slice(AI_TERMINAL_PREFIX.length); + return agentIds.includes(agentId) ? agentId : null; +} + +function selectedAgentIdForTask(task: { + agentIds: string[]; + selectedAgentId?: string; +}): string | null { + return task.selectedAgentId && task.agentIds.includes(task.selectedAgentId) + ? task.selectedAgentId + : null; +} + +export function setActiveTask(id: string): void { + const task = store.tasks[id]; + const terminal = store.terminals[id]; + if (!task && !terminal) return; + let activeAgentId: string | null = null; + if (task) { + activeAgentId = + focusedAgentIdForTask(id, task.agentIds) ?? + selectedAgentIdForTask(task) ?? + (store.activeAgentId && task.agentIds.includes(store.activeAgentId) + ? store.activeAgentId + : (task.agentIds[0] ?? null)); + if (activeAgentId) setStore('tasks', id, 'selectedAgentId', activeAgentId); + } + setStore('activeTaskId', id); + setStore('activeAgentId', activeAgentId); +} + +export function setActiveAgent(agentId: string): void { + setStore('activeAgentId', agentId); + const taskId = store.activeTaskId; + const task = taskId ? store.tasks[taskId] : undefined; + if (task?.agentIds.includes(agentId)) { + setStore('tasks', taskId as string, 'selectedAgentId', agentId); + } +} diff --git a/src/store/agents.ts b/src/store/agents.ts index e1854981..f7a26c29 100644 --- a/src/store/agents.ts +++ b/src/store/agents.ts @@ -158,16 +158,6 @@ export function removeCustomAgent(agentId: string): void { void refreshAvailableAgents(); } -export function updateCustomAgent(agentId: string, updated: AgentDef): void { - setStore( - produce((s) => { - const idx = s.customAgents.findIndex((a) => a.id === agentId); - if (idx >= 0) s.customAgents[idx] = updated; - }), - ); - void refreshAvailableAgents(); -} - /** Rebuild availableAgents from backend defaults + custom agents. */ async function refreshAvailableAgents(): Promise { const defaults = await invoke(IPC.ListAgents); diff --git a/src/store/focus.test.ts b/src/store/focus.test.ts index 3a565852..775ffeab 100644 --- a/src/store/focus.test.ts +++ b/src/store/focus.test.ts @@ -76,7 +76,7 @@ vi.mock('./sidebar-order', () => ({ computeSidebarTaskOrder: vi.fn(() => mockStore.taskOrder), })); -vi.mock('./tasks', () => ({ +vi.mock('./task-uncollapse', () => ({ uncollapseTask: vi.fn((id: string) => { if (mockStore.tasks[id]) mockStore.tasks[id].collapsed = false; }), diff --git a/src/store/focus.ts b/src/store/focus.ts index 28937b54..6e741383 100644 --- a/src/store/focus.ts +++ b/src/store/focus.ts @@ -1,8 +1,8 @@ import { batch } from 'solid-js'; import { store, setStore } from './core'; -import { setActiveTask } from './navigation'; +import { setActiveTask } from './active-task'; import { computeSidebarTaskOrder } from './sidebar-order'; -import { uncollapseTask } from './tasks'; +import { uncollapseTask } from './task-uncollapse'; // Imperative focus registry: components register focus callbacks on mount const focusRegistry = new Map void>(); @@ -265,10 +265,6 @@ export function unfocusPlaceholder(): void { setStore('placeholderFocused', false); } -export function setSidebarFocusedProjectId(id: string | null): void { - setStore('sidebarFocusedProjectId', id); -} - function focusTaskPanel(taskId: string, panel: string): void { setActiveTask(taskId); setTaskFocusedPanel(taskId, panel); diff --git a/src/store/navigation.ts b/src/store/navigation.ts index ffb9de05..18564e6e 100644 --- a/src/store/navigation.ts +++ b/src/store/navigation.ts @@ -1,64 +1,11 @@ import { store, setStore } from './core'; +import { setActiveTask } from './active-task'; import { getTaskFocusedPanel, setTaskFocusedPanel } from './focus'; import { showNotification } from './notification'; import { pickAndAddProject } from './projects'; import { reorderTask } from './tasks'; -const AI_TERMINAL_PREFIX = 'ai-terminal:'; - -function focusedAgentIdForTask(taskId: string, agentIds: string[]): string | null { - const panel = store.focusedPanel[taskId]; - if (!panel?.startsWith(AI_TERMINAL_PREFIX)) return null; - const agentId = panel.slice(AI_TERMINAL_PREFIX.length); - return agentIds.includes(agentId) ? agentId : null; -} - -function selectedAgentIdForTask(task: { - agentIds: string[]; - selectedAgentId?: string; -}): string | null { - return task.selectedAgentId && task.agentIds.includes(task.selectedAgentId) - ? task.selectedAgentId - : null; -} - -export function setActiveTask(id: string): void { - const task = store.tasks[id]; - const terminal = store.terminals[id]; - if (!task && !terminal) return; - let activeAgentId: string | null = null; - if (task) { - activeAgentId = - focusedAgentIdForTask(id, task.agentIds) ?? - selectedAgentIdForTask(task) ?? - (store.activeAgentId && task.agentIds.includes(store.activeAgentId) - ? store.activeAgentId - : (task.agentIds[0] ?? null)); - if (activeAgentId) setStore('tasks', id, 'selectedAgentId', activeAgentId); - } - setStore('activeTaskId', id); - setStore('activeAgentId', activeAgentId); -} - -export function setActiveAgent(agentId: string): void { - setStore('activeAgentId', agentId); - const taskId = store.activeTaskId; - const task = taskId ? store.tasks[taskId] : undefined; - if (task?.agentIds.includes(agentId)) { - setStore('tasks', taskId as string, 'selectedAgentId', agentId); - } -} - -export function navigateAgent(direction: 'up' | 'down'): void { - const { activeTaskId, activeAgentId } = store; - if (!activeTaskId) return; - const task = store.tasks[activeTaskId]; - if (!task) return; - const idx = activeAgentId ? task.agentIds.indexOf(activeAgentId) : -1; - const next = - direction === 'up' ? Math.max(0, idx - 1) : Math.min(task.agentIds.length - 1, idx + 1); - setStore('activeAgentId', task.agentIds[next]); -} +export { setActiveAgent, setActiveTask } from './active-task'; export function moveActiveTask(direction: 'left' | 'right'): void { const { taskOrder, activeTaskId } = store; diff --git a/src/store/persistence.ts b/src/store/persistence.ts index 3212b7ce..852264f9 100644 --- a/src/store/persistence.ts +++ b/src/store/persistence.ts @@ -2,7 +2,7 @@ import { produce } from 'solid-js/store'; import { invoke } from '../lib/ipc'; import { IPC } from '../../electron/ipc/channels'; import { store, setStore } from './core'; -import { randomPastelColor } from './projects'; +import { randomPastelColor } from './project-color'; import { markAgentSpawned } from './taskStatus'; import { getLocalDateKey } from '../lib/date'; import type { @@ -19,7 +19,7 @@ import { DEFAULT_TERMINAL_FONT } from '../lib/fonts'; import { isLookPreset } from '../lib/look'; import { validateCustomTheme, parseThemeCss, themeToCss } from '../lib/custom-theme'; import type { CustomTheme } from '../lib/custom-theme'; -import { syncTerminalCounter } from './terminals'; +import { syncTerminalCounterFromState } from './terminal-counter'; const RESTORED_AGENT_SPAWN_STAGGER_MS = 1_000; @@ -823,7 +823,7 @@ export async function loadState(): Promise { markAgentSpawned(agentId); } - syncTerminalCounter(); + syncTerminalCounterFromState(store.taskOrder, store.terminals); // Await migration of any customThemes found in state.json to individual CSS files. // Runs after the produce block so it can be properly awaited. loadCustomThemes() in diff --git a/src/store/project-color.ts b/src/store/project-color.ts new file mode 100644 index 00000000..93ef3e68 --- /dev/null +++ b/src/store/project-color.ts @@ -0,0 +1,6 @@ +export const PASTEL_HUES = [0, 30, 60, 120, 180, 210, 260, 300, 330]; + +export function randomPastelColor(): string { + const hue = PASTEL_HUES[Math.floor(Math.random() * PASTEL_HUES.length)]; + return `hsl(${hue}, 70%, 75%)`; +} diff --git a/src/store/project-removal.ts b/src/store/project-removal.ts new file mode 100644 index 00000000..0ee05072 --- /dev/null +++ b/src/store/project-removal.ts @@ -0,0 +1,29 @@ +import { store } from './core'; +import { closeTask } from './tasks'; +import { removeProject } from './projects'; + +export async function removeProjectWithTasks(projectId: string): Promise { + // Collect task IDs belonging to this project BEFORE removing anything + const taskIds = store.taskOrder.filter((tid) => store.tasks[tid]?.projectId === projectId); + const collapsedTaskIds = store.collapsedTaskOrder.filter( + (tid) => store.tasks[tid]?.projectId === projectId, + ); + + // Close tasks sequentially to avoid concurrent git operations on the same repo. + // Must happen before removeProject() since closeTask needs the project path. + // Coordinators must come last so their children are already closed first. + const allIds = [...taskIds, ...collapsedTaskIds]; + const isCoordinator = (tid: string) => store.tasks[tid]?.coordinatorMode === true; + const ordered = [...allIds.filter((tid) => !isCoordinator(tid)), ...allIds.filter(isCoordinator)]; + for (const tid of ordered) { + // closeTask handles and stores its own errors, so this should not throw. + await closeTask(tid); + } + + // If any tasks failed to close, keep the project so users can retry. + const hasRemainingTasks = allIds.some((tid) => store.tasks[tid]?.projectId === projectId); + if (hasRemainingTasks) return; + + // Now remove the project itself + removeProject(projectId); +} diff --git a/src/store/projects.ts b/src/store/projects.ts index db38e044..c8141669 100644 --- a/src/store/projects.ts +++ b/src/store/projects.ts @@ -3,16 +3,11 @@ import { openDialog } from '../lib/dialog'; import { invoke } from '../lib/ipc'; import { IPC } from '../../electron/ipc/channels'; import { store, setStore } from './core'; -import { closeTask } from './tasks'; import type { Project } from './types'; import { sanitizeBranchPrefix } from '../lib/branch-name'; +import { randomPastelColor } from './project-color'; -export const PASTEL_HUES = [0, 30, 60, 120, 180, 210, 260, 300, 330]; - -export function randomPastelColor(): string { - const hue = PASTEL_HUES[Math.floor(Math.random() * PASTEL_HUES.length)]; - return `hsl(${hue}, 70%, 75%)`; -} +export { PASTEL_HUES } from './project-color'; export function getProject(projectId: string): Project | undefined { return store.projects.find((p) => p.id === projectId); @@ -102,32 +97,6 @@ export function getProjectPath(projectId: string): string | undefined { return store.projects.find((p) => p.id === projectId)?.path; } -export async function removeProjectWithTasks(projectId: string): Promise { - // Collect task IDs belonging to this project BEFORE removing anything - const taskIds = store.taskOrder.filter((tid) => store.tasks[tid]?.projectId === projectId); - const collapsedTaskIds = store.collapsedTaskOrder.filter( - (tid) => store.tasks[tid]?.projectId === projectId, - ); - - // Close tasks sequentially to avoid concurrent git operations on the same repo. - // Must happen before removeProject() since closeTask needs the project path. - // Coordinators must come last so their children are already closed first. - const allIds = [...taskIds, ...collapsedTaskIds]; - const isCoordinator = (tid: string) => store.tasks[tid]?.coordinatorMode === true; - const ordered = [...allIds.filter((tid) => !isCoordinator(tid)), ...allIds.filter(isCoordinator)]; - for (const tid of ordered) { - // closeTask handles and stores its own errors, so this should not throw. - await closeTask(tid); - } - - // If any tasks failed to close, keep the project so users can retry. - const hasRemainingTasks = allIds.some((tid) => store.tasks[tid]?.projectId === projectId); - if (hasRemainingTasks) return; - - // Now remove the project itself - removeProject(projectId); -} - export function projectIsGitRepo(projectId: string): boolean { return getProject(projectId)?.isGitRepo !== false; } diff --git a/src/store/store.ts b/src/store/store.ts index 4c499b4c..a5fad223 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -4,7 +4,6 @@ export { getProject, addProject, removeProject, - removeProjectWithTasks, updateProject, getProjectPath, getProjectBranchPrefix, @@ -15,6 +14,7 @@ export { projectIsGitRepo, PASTEL_HUES, } from './projects'; +export { removeProjectWithTasks } from './project-removal'; export { loadAgents, addAgentToTask, @@ -24,7 +24,6 @@ export { switchAgent, addCustomAgent, removeCustomAgent, - updateCustomAgent, } from './agents'; export { createTask, @@ -48,13 +47,11 @@ export { closeShell, hasDirectTask, collapseTask, - uncollapseTask, getGitHubDropDefaults, setNewTaskDropUrl, setNewTaskPrefillPrompt, setPlanContent, setStepsContent, - setTaskStepsEnabled, setTaskLastInputAt, initMCPListeners, getCoordinatorCloseWarning, @@ -65,10 +62,10 @@ export { markTaskMcpError, retryTaskMcpStartup, } from './tasks'; +export { uncollapseTask } from './task-uncollapse'; export { setActiveTask, setActiveAgent, - navigateAgent, moveActiveTask, jumpToTask, toggleNewTaskDialog, @@ -95,12 +92,10 @@ export { toggleHelpDialog, toggleSettingsDialog, sendActivePrompt, - setSidebarFocusedProjectId, } from './focus'; export type { PanelId, PendingAction, TaskViewportVisibility } from './types'; export { saveState, loadState, loadCustomThemes } from './persistence'; export { - getGlobalScale, adjustGlobalScale, resetGlobalScale, getPanelUserSize, @@ -114,7 +109,6 @@ export { toggleTaskFocusMode, setTaskSplitMode, setTerminalFont, - setThemePreset, applyAppearanceMode, markCustomThemesReady, setAppearanceMode, @@ -122,7 +116,6 @@ export { setDarkTheme, saveCustomTheme, deleteCustomTheme, - activateCustomTheme, setAutoTrustFolders, setShowPlans, setShowPromptInput, @@ -168,12 +161,7 @@ export type { TaskAttentionState, TaskDotStatus } from './taskStatus'; export { showNotification, clearNotification } from './notification'; export { startPrChecksSubscription, getPrChecks, type PrChecksState } from './pr-checks'; export { getCompletedTasksTodayCount, getMergedLineTotals } from './completion'; -export { - createTerminal, - closeTerminal, - updateTerminalName, - syncTerminalCounter, -} from './terminals'; +export { createTerminal, closeTerminal, updateTerminalName } from './terminals'; export { startRemoteAccess, stopRemoteAccess, refreshRemoteStatus } from './remote'; export { updateStatus, diff --git a/src/store/task-uncollapse.ts b/src/store/task-uncollapse.ts new file mode 100644 index 00000000..127dcc18 --- /dev/null +++ b/src/store/task-uncollapse.ts @@ -0,0 +1,66 @@ +import { produce } from 'solid-js/store'; +import { store, setStore } from './core'; +import { markAgentSpawned, rescheduleTaskStatusPolling } from './taskStatus'; +import type { Agent } from './types'; + +const RESTORED_AGENT_SPAWN_STAGGER_MS = 1_000; + +export function uncollapseTask(taskId: string): void { + const task = store.tasks[taskId]; + if (!task || !task.collapsed) return; + + const savedDefs = + task.savedAgentDefs && task.savedAgentDefs.length > 0 + ? task.savedAgentDefs + : task.savedAgentDef + ? [task.savedAgentDef] + : []; + const restoredAgents = savedDefs.map((def) => ({ id: crypto.randomUUID(), def })); + const selectedAgentIndex = task.savedSelectedAgentIndex ?? 0; + const promptedAgentIndexes = task.savedPromptedAgentIndexes ?? []; + + setStore( + produce((s) => { + const t = s.tasks[taskId]; + t.collapsed = false; + s.collapsedTaskOrder = s.collapsedTaskOrder.filter((id) => id !== taskId); + s.taskOrder.push(taskId); + s.activeTaskId = taskId; + + for (let i = 0; i < restoredAgents.length; i++) { + const { id: agentId, def } = restoredAgents[i]; + const agent: Agent = { + id: agentId, + taskId, + def, + resumed: true, + status: 'running', + exitCode: null, + signal: null, + lastOutput: [], + generation: 0, + spawnDelayMs: + restoredAgents.length > 1 && i > 0 ? i * RESTORED_AGENT_SPAWN_STAGGER_MS : undefined, + }; + s.agents[agentId] = agent; + } + + t.agentIds = restoredAgents.map((agent) => agent.id); + const promptedAgentIds = promptedAgentIndexes + .map((index) => t.agentIds[index]) + .filter((id): id is string => Boolean(id)); + t.promptedAgentIds = promptedAgentIds.length > 0 ? promptedAgentIds : undefined; + t.selectedAgentId = t.agentIds[selectedAgentIndex] ?? t.agentIds[0]; + t.savedAgentDef = undefined; + t.savedAgentDefs = undefined; + t.savedSelectedAgentIndex = undefined; + t.savedPromptedAgentIndexes = undefined; + s.activeAgentId = t.selectedAgentId ?? null; + }), + ); + + if (restoredAgents.length > 0) { + for (const { id } of restoredAgents) markAgentSpawned(id); + rescheduleTaskStatusPolling(); + } +} diff --git a/src/store/tasks.ts b/src/store/tasks.ts index 0ec3ec7f..9dcad62b 100644 --- a/src/store/tasks.ts +++ b/src/store/tasks.ts @@ -484,7 +484,6 @@ export async function retryCloseTask(taskId: string): Promise { } const REMOVE_ANIMATION_MS = 300; -const RESTORED_AGENT_SPAWN_STAGGER_MS = 1_000; function removeTaskFromStore(taskId: string, agentIds: string[]): void { recordTaskCompleted(); @@ -893,66 +892,6 @@ export async function collapseTask(taskId: string): Promise { rescheduleTaskStatusPolling(); } -export function uncollapseTask(taskId: string): void { - const task = store.tasks[taskId]; - if (!task || !task.collapsed) return; - - const savedDefs = - task.savedAgentDefs && task.savedAgentDefs.length > 0 - ? task.savedAgentDefs - : task.savedAgentDef - ? [task.savedAgentDef] - : []; - const restoredAgents = savedDefs.map((def) => ({ id: crypto.randomUUID(), def })); - const selectedAgentIndex = task.savedSelectedAgentIndex ?? 0; - const promptedAgentIndexes = task.savedPromptedAgentIndexes ?? []; - - setStore( - produce((s) => { - const t = s.tasks[taskId]; - t.collapsed = false; - s.collapsedTaskOrder = s.collapsedTaskOrder.filter((id) => id !== taskId); - s.taskOrder.push(taskId); - s.activeTaskId = taskId; - - for (let i = 0; i < restoredAgents.length; i++) { - const { id: agentId, def } = restoredAgents[i]; - const agent: Agent = { - id: agentId, - taskId, - def, - resumed: true, - status: 'running', - exitCode: null, - signal: null, - lastOutput: [], - generation: 0, - spawnDelayMs: - restoredAgents.length > 1 && i > 0 ? i * RESTORED_AGENT_SPAWN_STAGGER_MS : undefined, - }; - s.agents[agentId] = agent; - } - - t.agentIds = restoredAgents.map((agent) => agent.id); - const promptedAgentIds = promptedAgentIndexes - .map((index) => t.agentIds[index]) - .filter((id): id is string => Boolean(id)); - t.promptedAgentIds = promptedAgentIds.length > 0 ? promptedAgentIds : undefined; - t.selectedAgentId = t.agentIds[selectedAgentIndex] ?? t.agentIds[0]; - t.savedAgentDef = undefined; - t.savedAgentDefs = undefined; - t.savedSelectedAgentIndex = undefined; - t.savedPromptedAgentIndexes = undefined; - s.activeAgentId = t.selectedAgentId ?? null; - }), - ); - - if (restoredAgents.length > 0) { - for (const { id } of restoredAgents) markAgentSpawned(id); - rescheduleTaskStatusPolling(); - } -} - // --- GitHub drop-to-create helpers --- /** Find best matching project by comparing repo name to project directory basenames. */ @@ -1373,9 +1312,3 @@ export function setStepsContent(taskId: string, steps: unknown[] | null): void { export function setTaskLastInputAt(taskId: string): void { setStore('tasks', taskId, 'lastInputAt', new Date().toISOString()); } - -/** Toggles steps tracking for a task and remembers the choice as the new default. */ -export function setTaskStepsEnabled(taskId: string, enabled: boolean): void { - setStore('tasks', taskId, 'stepsEnabled', enabled || undefined); - setStore('showSteps', enabled); // remember as default for future tasks -} diff --git a/src/store/terminal-counter.ts b/src/store/terminal-counter.ts new file mode 100644 index 00000000..feaf238d --- /dev/null +++ b/src/store/terminal-counter.ts @@ -0,0 +1,29 @@ +import type { Terminal } from './types'; + +let terminalCounter = 0; +let lastCreateTime = 0; + +export function recordTerminalCreateAttempt(now = Date.now()): boolean { + if (now - lastCreateTime < 300) return false; + lastCreateTime = now; + return true; +} + +export function nextTerminalName(): string { + terminalCounter++; + return `Terminal ${terminalCounter}`; +} + +export function syncTerminalCounterFromState( + taskOrder: readonly string[], + terminals: Record, +): void { + let max = 0; + for (const id of taskOrder) { + const terminal = terminals[id]; + if (!terminal) continue; + const match = terminal.name.match(/^Terminal (\d+)$/); + if (match) max = Math.max(max, Number(match[1])); + } + terminalCounter = max; +} diff --git a/src/store/terminals.ts b/src/store/terminals.ts index ae471823..966541f4 100644 --- a/src/store/terminals.ts +++ b/src/store/terminals.ts @@ -6,21 +6,15 @@ import { clearAgentActivity } from './taskStatus'; import { triggerFocus, getTaskFocusedPanel } from './focus'; import type { Terminal } from './types'; import { warn as logWarn } from '../lib/log'; - -let terminalCounter = 0; -let lastCreateTime = 0; +import { nextTerminalName, recordTerminalCreateAttempt } from './terminal-counter'; const REMOVE_ANIMATION_MS = 300; export function createTerminal(): void { - const now = Date.now(); - if (now - lastCreateTime < 300) return; - lastCreateTime = now; - - terminalCounter++; + if (!recordTerminalCreateAttempt()) return; const id = crypto.randomUUID(); const agentId = crypto.randomUUID(); - const name = `Terminal ${terminalCounter}`; + const name = nextTerminalName(); const terminal: Terminal = { id, name, agentId }; @@ -103,13 +97,3 @@ export function updateTerminalName(terminalId: string, name: string): void { } /** Restore the auto-increment counter from persisted state. */ -export function syncTerminalCounter(): void { - let max = 0; - for (const id of store.taskOrder) { - const t = store.terminals[id]; - if (!t) continue; - const match = t.name.match(/^Terminal (\d+)$/); - if (match) max = Math.max(max, Number(match[1])); - } - terminalCounter = max; -} diff --git a/src/store/ui.ts b/src/store/ui.ts index cd5e1779..b2161c3a 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -29,12 +29,6 @@ const MIN_SCALE = 0.5; const MAX_SCALE = 2.0; const SCALE_STEP = 0.1; -// --- Global Scale --- - -export function getGlobalScale(): number { - return store.globalScale; -} - export function adjustGlobalScale(delta: 1 | -1): void { const current = store.globalScale; const next = @@ -90,10 +84,6 @@ export function setTerminalFont(terminalFont: string): void { setStore('terminalFont', terminalFont); } -export function setThemePreset(themePreset: LookPreset): void { - setStore('themePreset', themePreset); -} - export function applyAppearanceMode(): void { const isDark = osIsDark(); const mode = store.appearanceMode; @@ -158,10 +148,6 @@ export async function deleteCustomTheme(id: string): Promise { applyAppearanceMode(); } -export function activateCustomTheme(id: string | null): void { - setStore('activeCustomThemeId', id); -} - export function setAutoTrustFolders(autoTrustFolders: boolean): void { setStore('autoTrustFolders', autoTrustFolders); }