From f713e3db7996047d3bb31528a814f0f6ec985b12 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:28:21 +0000 Subject: [PATCH 01/15] fix: restore workspace bootstrap and agent dispatch behavior Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/components/TerminalPane.tsx | 7 + frontend/src/store.ts | 227 ++++++++++++++++++----- src/ipc.zig | 55 +++++- src/pty.zig | 23 ++- 4 files changed, 254 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/TerminalPane.tsx b/frontend/src/components/TerminalPane.tsx index 831c171..00805c0 100644 --- a/frontend/src/components/TerminalPane.tsx +++ b/frontend/src/components/TerminalPane.tsx @@ -37,6 +37,7 @@ const TERMINAL_THEME = { // Global registry so PTY data handler can find terminals by sessionId export const terminalRegistry = new Map(); +const terminalSnapshotRegistry = new Map(); interface Props { pane: PaneState; @@ -74,6 +75,11 @@ export function TerminalPane({ pane }: Props) { terminal.loadAddon(serializeAddon); terminal.open(container); + const snapshot = terminalSnapshotRegistry.get(pane.sessionId); + if (snapshot) { + terminal.write(snapshot); + } + termRef.current = terminal; fitRef.current = fitAddon; terminalRegistry.set(pane.sessionId, terminal); @@ -95,6 +101,7 @@ export function TerminalPane({ pane }: Props) { return () => { resizeObserver.disconnect(); + terminalSnapshotRegistry.set(pane.sessionId, serializeAddon.serialize()); terminalRegistry.delete(pane.sessionId); terminal.dispose(); }; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index bb395dc..34b6463 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -95,11 +95,13 @@ export interface SchedulerSettings { defaultAgentId: string; } -export const schedulerSettings = signal({ +const DEFAULT_SCHEDULER_SETTINGS: SchedulerSettings = { concurrency: 4, autoDispatch: true, defaultAgentId: 'claude', -}); +}; + +export const schedulerSettings = signal({ ...DEFAULT_SCHEDULER_SETTINGS }); // -- Agent Pool (derived from agents signal) -- export const agentPool = computed(() => { @@ -464,7 +466,12 @@ export const activeSpacePanes = computed(() => // Only non-embedded panes go into the tiling grid export const activeSpaceGridPanes = computed(() => - panes.value.filter(p => p.spaceId === activeSpaceId.value && !p.embedded) + panes.value.filter(p => + p.spaceId === activeSpaceId.value && + !p.embedded && + p.id !== expandedPaneId.value && + !popoutPanes.value.has(p.id) + ) ); export const activeSpaceNotes = computed(() => @@ -638,6 +645,40 @@ function convertTask(raw: any): TaskEntity { }; } +function normalizeSchedulerSettings(raw: any): SchedulerSettings { + return { + concurrency: Math.max(1, Number(raw?.concurrency ?? DEFAULT_SCHEDULER_SETTINGS.concurrency) || DEFAULT_SCHEDULER_SETTINGS.concurrency), + autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, + }; +} + +async function loadSchedulerSettings(workspaceId: string): Promise { + try { + const settings = await ipc.call('scheduler.getSettings', { workspace_id: workspaceId }); + schedulerSettings.value = normalizeSchedulerSettings(settings); + } catch { + schedulerSettings.value = { ...DEFAULT_SCHEDULER_SETTINGS }; + } +} + +async function ensureWorkspace(): Promise { + if (currentWorkspaceId.value) return currentWorkspaceId.value; + + try { + const created = await ipc.call<{ id: string }>('workspace.create', { + name: 'Default', + path: workspacePath.value, + }); + currentWorkspaceId.value = created.id; + await loadSchedulerSettings(created.id); + return created.id; + } catch (err) { + console.error('workspace.create failed:', err); + return null; + } +} + // -- Internal helpers -- let paneCounter = 0; @@ -655,7 +696,10 @@ async function spawnTerminal( command?: string, prompt?: string, ): Promise { - const space = activeSpace.value; + let space = activeSpace.value; + if (!space) { + space = await createSpace('Default'); + } if (!space) return null; // Use workspace path as cwd, default to home/.claude for agents @@ -771,7 +815,10 @@ export async function createTask( priority: 'low' | 'medium' | 'high' = 'medium', parentTaskId?: string ): Promise { - const space = activeSpace.value; + let space = activeSpace.value; + if (!space) { + space = await createSpace('Default'); + } if (!space) return null; try { @@ -971,7 +1018,7 @@ function handleAgentCompletion(sessionId: string): void { // -- Workspace Path (default working directory) -- export async function setWorkspacePath(path: string): Promise { - const wsId = currentWorkspaceId.value; + const wsId = await ensureWorkspace(); if (!wsId) return; try { await ipc.call('workspace.setPath', { workspace_id: wsId, path }); @@ -1116,10 +1163,24 @@ export function focusPane(paneId: string) { focusedPaneId.value = paneId; } -export function createSpace(name: string) { - const id = `space-${Date.now()}`; - spaces.value = [...spaces.value, { id, name }]; - activeSpaceId.value = id; +export async function createSpace(name: string, id?: string): Promise { + const workspaceId = await ensureWorkspace(); + if (!workspaceId) return null; + + try { + const created = await ipc.call<{ id: string }>('space.create', { + workspace_id: workspaceId, + name, + id, + }); + const space = { id: created.id, name }; + spaces.value = [...spaces.value, space]; + activeSpaceId.value = space.id; + return space; + } catch (err) { + console.error('space.create failed:', err); + return null; + } } export function getLinkedPane(paneId: string): PaneState | undefined { @@ -1240,6 +1301,7 @@ export async function hydrateState(): Promise { const restoredTasks = new Map(); const restoredAgents = new Map(); currentWorkspaceId.value = ws.id; + await loadSchedulerSettings(ws.id); // Restore workspace path (default cwd) workspacePath.value = ws.path || ''; @@ -1339,10 +1401,12 @@ export async function hydrateState(): Promise { } // Fall back to creating default space - createSpace('Default'); + await ensureWorkspace(); + await createSpace('Default'); } catch (err) { console.error('hydrateState failed:', err); - createSpace('Default'); + await ensureWorkspace(); + await createSpace('Default'); } } @@ -1422,6 +1486,34 @@ export function exportWorkspaceToJson(): string { return JSON.stringify(exported, null, 2); } +function normalizeImportedTask(task: TaskEntity) { + if (task.status === 'done' || task.queueStatus === 'completed') { + return { status: 'done' as const, queueStatus: 'completed' as const }; + } + + if (task.queueStatus === 'queued' || task.queueStatus === 'dispatched' || task.status === 'doing') { + return { status: 'todo' as const, queueStatus: 'queued' as const }; + } + + return { status: 'todo' as const, queueStatus: 'none' as const }; +} + +async function clearCurrentWorkspaceState(): Promise { + const existingSpaces = [...spaces.value]; + for (const space of existingSpaces) { + await ipc.call('space.delete', { id: space.id }); + } + + spaces.value = []; + panes.value = []; + tasks.value = new Map(); + agents.value = new Map(); + notes.value = []; + focusedPaneId.value = null; + expandedPaneId.value = null; + popoutPanes.value = new Map(); +} + // Import workspace state from JSON (frontend-only) export async function importWorkspaceFromJson(json: string): Promise { const data = JSON.parse(json) as ExportedWorkspace; @@ -1430,60 +1522,99 @@ export async function importWorkspaceFromJson(json: string): Promise { throw new Error(`Unsupported export version: ${data.version}`); } - // Clear current state - const spaceId = activeSpaceId.value; - if (!spaceId) return; + const workspaceId = await ensureWorkspace(); + if (!workspaceId) { + throw new Error('No workspace available'); + } + + await clearCurrentWorkspaceState(); + await setWorkspacePath(data.workspace.path ?? ''); + + const importedSettings = normalizeSchedulerSettings(data.schedulerSettings); + await ipc.call('scheduler.setSettings', { + workspace_id: workspaceId, + concurrency: importedSettings.concurrency, + auto_dispatch: importedSettings.autoDispatch, + default_agent_id: importedSettings.defaultAgentId, + }); + schedulerSettings.value = importedSettings; + + const spaceIdMap = new Map(); + for (const space of data.spaces) { + const created = await createSpace(space.name, space.id); + if (created) { + spaceIdMap.set(space.id, created.id); + } + } + + if (spaceIdMap.size === 0) { + const fallback = await createSpace('Default'); + if (fallback) { + spaceIdMap.set('default', fallback.id); + } + } + + const taskIdMap = new Map(); + const pendingTasks = [...data.tasks]; + while (pendingTasks.length > 0) { + let progressed = false; + + for (let i = 0; i < pendingTasks.length; i++) { + const task = pendingTasks[i]; + const mappedSpaceId = spaceIdMap.get(task.spaceId) ?? activeSpaceId.value; + const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; - // Import tasks - for (const task of data.tasks) { - // Create task in backend - try { - await ipc.call('task.create', { + if (task.parentTaskId && !mappedParentTaskId) continue; + if (!mappedSpaceId) continue; + + const created = await ipc.call('task.create', { id: task.id, - space_id: spaceId, + space_id: mappedSpaceId, title: task.title, description: task.description, - status: task.status, priority: task.priority, - queue_status: task.queueStatus, - parent_task_id: task.parentTaskId, + parent_task_id: mappedParentTaskId, }); - } catch (e) { - console.warn('Failed to import task:', task.id, e); + + const createdId = created.id ?? task.id; + taskIdMap.set(task.id, createdId); + + const normalized = normalizeImportedTask(task); + if (normalized.status !== 'todo' || normalized.queueStatus !== 'none') { + await ipc.call('task.update', { + id: createdId, + status: normalized.status, + queue_status: normalized.queueStatus, + }); + } + + pendingTasks.splice(i, 1); + i--; + progressed = true; } - } - // Import notes - for (const note of data.notes) { - createNote(note.text); + if (!progressed) { + throw new Error('Could not resolve imported task hierarchy'); + } } - // Update scheduler settings - schedulerSettings.value = data.schedulerSettings; + notes.value = data.notes.map(note => ({ + ...note, + spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, + linkedPaneId: note.linkedPaneId ? taskIdMap.get(note.linkedPaneId) : undefined, + })); - // Re-hydrate to sync with backend await hydrateState(); } // Backend-backed export (full persistence) export async function exportWorkspace(workspaceId: string): Promise { - try { - const result = await ipc.call<{ json: string }>('workspace.export', { workspace_id: workspaceId }); - return result.json; - } catch (err) { - console.error('exportWorkspace failed:', err); - throw err; - } + void workspaceId; + return exportWorkspaceToJson(); } export async function importWorkspace(json: string): Promise { - try { - await ipc.call('workspace.import', { json }); - await hydrateState(); - } catch (err) { - console.error('importWorkspace failed:', err); - throw err; - } + await importWorkspaceFromJson(json); } // -- Auto dispatch effect (Pool-based, now with backend persistence) -- @@ -1498,7 +1629,7 @@ async function dispatchTaskToSlot(taskId: string, slotId: string): Promise const task = tasks.value.get(taskId); if (!task) return; - const agentProviderId = slot.agentProviderId; + const agentProviderId = schedulerSettings.value.defaultAgentId || slot.agentProviderId; const prompt = task.description || task.title; // 1. Assign task to agent via backend (handles bidirectional update) diff --git a/src/ipc.zig b/src/ipc.zig index 45f4b7c..882d014 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -440,9 +440,13 @@ pub const Server = struct { const workspace_id = getStr(params, "workspace_id") orelse return; const name = getStr(params, "name") orelse "Space"; const dir_path = getStr(params, "directory_path") orelse ""; + const requested_id = getStr(params, "id"); - const space_id = std.fmt.allocPrint(self.allocator, "sp-{d}", .{std.time.timestamp()}) catch return; - defer self.allocator.free(space_id); + const space_id = requested_id orelse blk: { + const generated = std.fmt.allocPrint(self.allocator, "sp-{d}", .{std.time.timestamp()}) catch return; + break :blk generated; + }; + defer if (requested_id == null) self.allocator.free(space_id); self.db.createSpace(space_id, workspace_id, name, dir_path) catch |err| { sendError(client, id, -32000, @errorName(err)); @@ -594,18 +598,40 @@ pub const Server = struct { const description = getStr(params, "description") orelse ""; const priority = getStr(params, "priority") orelse "medium"; const parent_task_id = getStr(params, "parent_task_id"); + const requested_id = getStr(params, "id"); - const task_id = std.fmt.allocPrint(self.allocator, "task-{d}", .{std.time.timestamp()}) catch return; - defer self.allocator.free(task_id); + const task_id = requested_id orelse blk: { + const generated = std.fmt.allocPrint(self.allocator, "task-{d}", .{std.time.timestamp()}) catch return; + break :blk generated; + }; + defer if (requested_id == null) self.allocator.free(task_id); self.db.createTask(task_id, space_id, title, description, priority, parent_task_id) catch |err| { sendError(client, id, -32000, @errorName(err)); return; }; - var resp_buf: [512]u8 = undefined; - const resp = std.fmt.bufPrint(&resp_buf, "{{\"id\":\"{s}\",\"space_id\":\"{s}\",\"title\":\"{s}\",\"status\":\"todo\",\"priority\":\"{s}\",\"queue_status\":\"none\"}}", .{ task_id, space_id, title, priority }) catch return; - sendResult(client, id, resp); + var resp_buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&resp_buf); + const writer = fbs.writer(); + writer.writeByte('{') catch return; + writer.writeAll("\"id\":") catch return; + writeJsonString(writer, task_id) catch return; + writer.writeAll(",\"space_id\":") catch return; + writeJsonString(writer, space_id) catch return; + writer.writeAll(",\"title\":") catch return; + writeJsonString(writer, title) catch return; + writer.writeAll(",\"description\":") catch return; + writeJsonString(writer, description) catch return; + writer.writeAll(",\"status\":\"todo\",\"priority\":") catch return; + writeJsonString(writer, priority) catch return; + writer.writeAll(",\"queue_status\":\"none\"") catch return; + if (parent_task_id) |parent| { + writer.writeAll(",\"parent_task_id\":") catch return; + writeJsonString(writer, parent) catch return; + } + writer.writeByte('}') catch return; + sendResult(client, id, fbs.getWritten()); } fn rpcTaskUpdate(self: *Server, params: std.json.ObjectMap, id: ?std.json.Value, client: *Client) void { @@ -1533,6 +1559,21 @@ fn mimeType(path: []const u8) []const u8 { return "application/octet-stream"; } +fn writeJsonString(writer: anytype, value: []const u8) !void { + try writer.writeByte('"'); + for (value) |ch| { + switch (ch) { + '"' => try writer.writeAll("\\\""), + '\\' => try writer.writeAll("\\\\"), + '\n' => try writer.writeAll("\\n"), + '\r' => try writer.writeAll("\\r"), + '\t' => try writer.writeAll("\\t"), + else => try writer.writeByte(ch), + } + } + try writer.writeByte('"'); +} + fn sendResult(client: *Client, id: ?std.json.Value, result: []const u8) void { var buf: [32768]u8 = undefined; var id_buf: [32]u8 = undefined; diff --git a/src/pty.zig b/src/pty.zig index c3b495d..fc2b800 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -60,7 +60,9 @@ pub fn spawn( if (pid == 0) { // Child process if (cwd) |dir| { - std.posix.chdir(dir) catch {}; + const resolved_dir = resolveWorkingDirectory(allocator, dir) catch dir; + defer if (resolved_dir.ptr != dir.ptr) allocator.free(resolved_dir); + std.posix.chdir(resolved_dir) catch {}; } // Set environment variables - these modify the process environment @@ -80,13 +82,13 @@ pub fn spawn( // Wrap command in login shell to get full environment const shell = std.posix.getenv("SHELL") orelse "/bin/zsh"; const wrapped_cmd = std.fmt.allocPrintSentinel(allocator, - "source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; {s}", + "exec {s}", .{cmd}, 0) catch { std.posix.exit(1); }; const argv = [_:null]?[*:0]const u8{ @ptrCast(shell), - "-l".ptr, + "-il".ptr, "-c".ptr, wrapped_cmd.ptr, null, @@ -125,6 +127,21 @@ pub fn spawn( }; } +fn resolveWorkingDirectory(allocator: std.mem.Allocator, dir: []const u8) ![]const u8 { + if (dir.len == 0 or dir[0] != '~') { + return dir; + } + + const home = std.posix.getenv("HOME") orelse return dir; + if (std.mem.eql(u8, dir, "~")) { + return allocator.dupe(u8, home); + } + if (dir.len > 1 and dir[1] == '/') { + return std.fmt.allocPrint(allocator, "{s}{s}", .{ home, dir[1..] }); + } + return dir; +} + extern "c" fn forkpty( master: *posix.fd_t, name: ?[*:0]u8, From dc925302376fa63b0e222cf4d02d59640736ac28 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:28:55 +0000 Subject: [PATCH 02/15] test: add frontend functional coverage Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/0672c178-4353-4190-8e32-4de6cdfa92bf Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- build.zig | 3 +- frontend/package-lock.json | 2219 ++++++++++++++++- frontend/package.json | 17 +- frontend/src/components/AgentDialog.test.tsx | 47 + .../ExecutionHistoryDialog.test.tsx | 30 + frontend/src/components/Sidebar.test.tsx | 103 + frontend/src/components/StatusBar.test.tsx | 54 + frontend/src/components/TaskDialog.test.tsx | 68 + frontend/src/components/TaskDialog.tsx | 8 +- frontend/src/store.test.ts | 167 ++ frontend/src/test/helpers.ts | 29 + frontend/src/test/setup.ts | 26 + frontend/vite.config.ts | 5 + src/db.zig | 139 ++ src/pty.zig | 66 +- src/test_main.zig | 5 + tests/run-functional-tests.sh | 7 + 17 files changed, 2874 insertions(+), 119 deletions(-) create mode 100644 frontend/src/components/AgentDialog.test.tsx create mode 100644 frontend/src/components/ExecutionHistoryDialog.test.tsx create mode 100644 frontend/src/components/Sidebar.test.tsx create mode 100644 frontend/src/components/StatusBar.test.tsx create mode 100644 frontend/src/components/TaskDialog.test.tsx create mode 100644 frontend/src/store.test.ts create mode 100644 frontend/src/test/helpers.ts create mode 100644 frontend/src/test/setup.ts create mode 100644 src/test_main.zig create mode 100755 tests/run-functional-tests.sh diff --git a/build.zig b/build.zig index 58fe4ce..6afe209 100644 --- a/build.zig +++ b/build.zig @@ -41,11 +41,12 @@ pub fn build(b: *std.Build) void { // Tests const unit_tests = b.addTest(.{ .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), + .root_source_file = b.path("src/test_main.zig"), .target = target, .optimize = optimize, }), }); + unit_tests.linkSystemLibrary("util"); unit_tests.linkSystemLibrary("sqlite3"); unit_tests.linkLibC(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9eb237f..3a0a337 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,10 +18,52 @@ }, "devDependencies": { "@preact/preset-vite": "^2.9.0", + "@testing-library/preact": "^3.2.4", + "jsdom": "^29.0.2", "typescript": "^5.7.0", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^4.1.3" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.8.tgz", + "integrity": "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.8.tgz", + "integrity": "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -289,6 +331,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -337,6 +389,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -779,6 +984,24 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1359,6 +1582,74 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/preact": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/preact/-/preact-3.2.4.tgz", + "integrity": "sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^8.11.1" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "preact": ">=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1366,6 +1657,129 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz", + "integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz", + "integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz", + "integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz", + "integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.3", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz", + "integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.3", + "@vitest/utils": "4.1.3", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz", + "integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.3", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xterm/addon-fit": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", @@ -1399,38 +1813,127 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, - "node_modules/babel-plugin-transform-hook-names": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", - "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.12.10" + "engines": { + "node": ">=8" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", - "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } }, - "node_modules/browserslist": { - "version": "4.28.2", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, @@ -1463,6 +1966,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001786", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", @@ -1484,6 +2037,53 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1508,6 +2108,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -1521,6 +2135,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1539,6 +2167,89 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1598,6 +2309,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.332", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", @@ -1618,6 +2344,67 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1677,6 +2464,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1695,6 +2492,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1710,6 +2523,26 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1720,30 +2553,494 @@ "node": ">=6.9.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", - "bin": { - "he": "bin/he" + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/htm": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", - "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", - "license": "Apache-2.0" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", @@ -1786,6 +3083,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1796,6 +3103,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1853,6 +3177,111 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1873,6 +3302,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -1894,78 +3333,302 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/preact": { - "version": "10.29.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", - "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": ">= 0.4" }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", - "fsevents": "~2.3.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "license": "ISC" }, "node_modules/simple-code-frame": { "version": "1.3.0", @@ -2007,6 +3670,71 @@ "node": ">=16" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2024,6 +3752,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.28" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2038,6 +3822,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", + "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2162,6 +3956,239 @@ "vite": "5.x || 6.x || 7.x || 8.x" } }, + "node_modules/vitest": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz", + "integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.3", + "@vitest/mocker": "4.1.3", + "@vitest/pretty-format": "4.1.3", + "@vitest/runner": "4.1.3", + "@vitest/snapshot": "4.1.3", + "@vitest/spy": "4.1.3", + "@vitest/utils": "4.1.3", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.3", + "@vitest/browser-preview": "4.1.3", + "@vitest/browser-webdriverio": "4.1.3", + "@vitest/coverage-istanbul": "4.1.3", + "@vitest/coverage-v8": "4.1.3", + "@vitest/ui": "4.1.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 330f261..9d0fd2b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,20 +6,25 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" }, "dependencies": { - "preact": "^10.25.0", - "htm": "^3.1.1", "@preact/signals": "^2.0.0", - "@xterm/xterm": "^5.5.0", "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-search": "^0.15.0", "@xterm/addon-serialize": "^0.13.0", - "@xterm/addon-search": "^0.15.0" + "@xterm/xterm": "^5.5.0", + "htm": "^3.1.1", + "preact": "^10.25.0" }, "devDependencies": { + "@preact/preset-vite": "^2.9.0", + "@testing-library/preact": "^3.2.4", + "jsdom": "^29.0.2", "typescript": "^5.7.0", "vite": "^6.0.0", - "@preact/preset-vite": "^2.9.0" + "vitest": "^4.1.3" } } diff --git a/frontend/src/components/AgentDialog.test.tsx b/frontend/src/components/AgentDialog.test.tsx new file mode 100644 index 0000000..6260a97 --- /dev/null +++ b/frontend/src/components/AgentDialog.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/preact'; +import { describe, expect, it, vi } from 'vitest'; +import { AgentDialog } from './AgentDialog'; +import * as store from '../store'; + +describe('AgentDialog', () => { + it('launches the selected agent and closes the dialog', async () => { + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + store.workspacePath.value = '/repo'; + + const onClose = vi.fn(); + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string) => { + if (method === 'pty.spawn') return { session_id: 'session-7' }; + throw new Error(`Unexpected method: ${method}`); + }); + + render(); + + await fireEvent.click(screen.getByText('Codex CLI')); + + await waitFor(() => { + expect(ipcCall).toHaveBeenCalledWith('pty.spawn', { + kind: 'agent', + space_id: 'space-1', + cwd: '/repo', + command: 'codex', + }); + }); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(store.panes.value.find((pane) => pane.agentName === 'Codex CLI')).toMatchObject({ + kind: 'agent', + sessionId: 'session-7', + }); + }); + + it('closes immediately when cancel is clicked', async () => { + const onClose = vi.fn(); + + render(); + + await fireEvent.click(screen.getByText('Cancel')); + + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/src/components/ExecutionHistoryDialog.test.tsx b/frontend/src/components/ExecutionHistoryDialog.test.tsx new file mode 100644 index 0000000..ef9779f --- /dev/null +++ b/frontend/src/components/ExecutionHistoryDialog.test.tsx @@ -0,0 +1,30 @@ +import { fireEvent, render, screen } from '@testing-library/preact'; +import { describe, expect, it, vi } from 'vitest'; +import { ExecutionHistoryDialog } from './ExecutionHistoryDialog'; +import * as store from '../store'; + +describe('ExecutionHistoryDialog', () => { + it('clears history after confirmation and closes on demand', async () => { + store.executionHistory.value = [ + { + id: 'exec-1', + agentId: 'claude', + agentName: 'Claude Code', + prompt: 'Run tests', + startedAt: Date.now(), + status: 'completed', + }, + ]; + + const onClose = vi.fn(); + vi.spyOn(window, 'confirm').mockReturnValue(true); + + render(); + + await fireEvent.click(screen.getByText('Clear')); + expect(store.executionHistory.value).toHaveLength(0); + + await fireEvent.click(screen.getByText('Close')); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/src/components/Sidebar.test.tsx b/frontend/src/components/Sidebar.test.tsx new file mode 100644 index 0000000..7a94d91 --- /dev/null +++ b/frontend/src/components/Sidebar.test.tsx @@ -0,0 +1,103 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/preact'; +import { describe, expect, it, vi } from 'vitest'; +import { Sidebar } from './Sidebar'; +import * as store from '../store'; + +function renderSidebar() { + const onAddTask = vi.fn(); + const onAddAgent = vi.fn(); + const onAddNote = vi.fn(); + + render(); + + return { onAddTask, onAddAgent, onAddNote }; +} + +describe('Sidebar', () => { + it('wires primary action buttons for the active space', async () => { + store.currentWorkspaceId.value = 'ws-1'; + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + + const { onAddTask, onAddAgent, onAddNote } = renderSidebar(); + + await fireEvent.click(screen.getByText('+ Task')); + await fireEvent.click(screen.getByText('+ Agent')); + await fireEvent.click(screen.getByText('+ Note')); + + expect(onAddTask).toHaveBeenCalledWith({ id: 'space-1', name: 'Default' }); + expect(onAddAgent).toHaveBeenCalledWith({ id: 'space-1', name: 'Default' }); + expect(onAddNote).toHaveBeenCalledWith({ id: 'space-1', name: 'Default' }); + }); + + it('toggles theme, creates a new space, and exports the workspace', async () => { + store.currentWorkspaceId.value = 'ws-1'; + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + + const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:workspace'); + const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); + const anchorClick = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string) => { + if (method === 'workspace.export') return { json: '{"workspace":true}' }; + throw new Error(`Unexpected method: ${method}`); + }); + + renderSidebar(); + + await fireEvent.click(screen.getByTitle('Light Mode')); + expect(document.documentElement.getAttribute('data-theme')).toBe('light'); + + await fireEvent.click(screen.getByTitle('New Space')); + const input = screen.getByPlaceholderText('Space name…'); + await fireEvent.input(input, { target: { value: 'QA' } }); + await fireEvent.keyDown(input, { key: 'Enter' }); + expect(store.spaces.value.some((space) => space.name === 'QA')).toBe(true); + + await fireEvent.click(screen.getByTitle('Export Workspace')); + + await waitFor(() => { + expect(ipcCall).toHaveBeenCalledWith('workspace.export', { workspace_id: 'ws-1' }); + expect(anchorClick).toHaveBeenCalledTimes(1); + }); + expect(createObjectURL).toHaveBeenCalledTimes(1); + expect(revokeObjectURL).toHaveBeenCalledWith('blob:workspace'); + }); + + it('imports a workspace file through the hidden file input', async () => { + store.currentWorkspaceId.value = 'ws-1'; + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string) => { + if (method === 'workspace.import') return null; + if (method === 'state.hydrate') { + return { + workspaces: [ + { + id: 'ws-1', + name: 'Workspace', + path: '/repo', + spaces: [ + { id: 'space-1', workspace_id: 'ws-1', name: 'Default', nodes: [], tasks: [], agents: [] }, + ], + }, + ], + settings: {}, + }; + } + throw new Error(`Unexpected method: ${method}`); + }); + + const { container } = render(); + + const input = container.querySelector('input[type="file"]') as HTMLInputElement; + const file = new File(['{"workspace":true}'], 'workspace.json', { type: 'application/json' }); + await fireEvent.change(input, { target: { files: [file] } }); + + await waitFor(() => { + expect(ipcCall).toHaveBeenCalledWith('workspace.import', { json: '{"workspace":true}' }); + expect(ipcCall).toHaveBeenCalledWith('state.hydrate', {}); + }); + }); +}); diff --git a/frontend/src/components/StatusBar.test.tsx b/frontend/src/components/StatusBar.test.tsx new file mode 100644 index 0000000..f635197 --- /dev/null +++ b/frontend/src/components/StatusBar.test.tsx @@ -0,0 +1,54 @@ +import { fireEvent, render, screen } from '@testing-library/preact'; +import { describe, expect, it, vi } from 'vitest'; +import { StatusBar } from './StatusBar'; +import * as store from '../store'; + +describe('StatusBar', () => { + it('renders scheduler summary buttons and forwards click handlers', async () => { + store.schedulerSettings.value = { concurrency: 2, autoDispatch: true, defaultAgentId: 'claude' }; + store.panes.value = [ + { id: 'task-1', kind: 'task', spaceId: 'space-1', taskStatus: 'todo' }, + { id: 'shell-1', kind: 'shell', spaceId: 'space-1', sessionStatus: 'running' }, + ]; + store.agents.value = new Map([ + ['slot-1', { + id: 'slot-1', + spaceId: 'space-1', + providerId: 'claude', + providerName: 'Claude Code', + status: 'running', + sortOrder: 0, + createdAt: 1, + }], + ]); + store.tasks.value = new Map([ + ['task-1', { + id: 'task-1', + spaceId: 'space-1', + title: 'Queued task', + description: 'Run me', + status: 'todo', + priority: 'medium', + queueStatus: 'queued', + queuedAt: 1, + sortOrder: 0, + createdAt: 1, + }], + ]); + + const onOpenSettings = vi.fn(); + const onOpenHistory = vi.fn(); + render(); + + expect(screen.getByText('1 task · 1 session')).toBeTruthy(); + expect(screen.getByText('1 queued')).toBeTruthy(); + expect(screen.getByText('1 running')).toBeTruthy(); + expect(screen.getByText('auto on')).toBeTruthy(); + + await fireEvent.click(screen.getByText('History')); + await fireEvent.click(screen.getByText('Settings')); + + expect(onOpenHistory).toHaveBeenCalledTimes(1); + expect(onOpenSettings).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/src/components/TaskDialog.test.tsx b/frontend/src/components/TaskDialog.test.tsx new file mode 100644 index 0000000..fb206c3 --- /dev/null +++ b/frontend/src/components/TaskDialog.test.tsx @@ -0,0 +1,68 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/preact'; +import { describe, expect, it, vi } from 'vitest'; +import { TaskDialog } from './TaskDialog'; +import * as store from '../store'; + +describe('TaskDialog', () => { + it('creates a task and asks the AI to summarize it', async () => { + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + store.schedulerSettings.value = { concurrency: 4, autoDispatch: false, defaultAgentId: 'claude' }; + + const onClose = vi.fn(); + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string, params?: Record) => { + if (method === 'task.create') { + return { + id: 'task-1', + space_id: 'space-1', + title: params?.title, + description: params?.description, + status: 'todo', + priority: params?.priority, + queue_status: 'none', + sort_order: 0, + created_at: 1, + }; + } + if (method === 'ai.summarize') return null; + throw new Error(`Unexpected method: ${method}`); + }); + + render(); + + await fireEvent.input(screen.getByPlaceholderText('e.g., Fix login bug, Add dark mode...'), { + target: { value: 'Cover startup dispatch' }, + }); + await fireEvent.click(screen.getByLabelText('High')); + await fireEvent.click(screen.getByText('Create')); + + await waitFor(() => { + expect(ipcCall).toHaveBeenCalledWith('ai.summarize', { + prompt: 'Cover startup dispatch', + pane_id: 'task-1', + }); + }); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(store.tasks.value.get('task-1')).toMatchObject({ + title: 'Cover startup dispatch', + priority: 'high', + }); + expect(store.panes.value.find((pane) => pane.id === 'task-1')).toMatchObject({ + taskTitle: 'Cover startup dispatch', + taskPriority: 'high', + }); + }); + + it('closes without creating a task when cancel is clicked', async () => { + const onClose = vi.fn(); + const ipcCall = vi.spyOn(store.ipc, 'call'); + + render(); + + await fireEvent.click(screen.getByText('Cancel')); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(ipcCall).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/TaskDialog.tsx b/frontend/src/components/TaskDialog.tsx index 0b05f5b..707b6e3 100644 --- a/frontend/src/components/TaskDialog.tsx +++ b/frontend/src/components/TaskDialog.tsx @@ -11,11 +11,11 @@ export function TaskDialog({ onClose }: Props) { useEffect(() => { titleRef.current?.focus(); }, []); - const submit = () => { + const submit = async () => { const title = titleRef.current?.value.trim(); if (!title) return; - const pane = createTask(title, '...', priority); + const pane = await createTask(title, '...', priority); onClose(); if (pane) { @@ -36,7 +36,7 @@ export function TaskDialog({ onClose }: Props) { ref={titleRef} class="dialog-input" placeholder="e.g., Fix login bug, Add dark mode..." - onKeyDown={(e) => { if (e.key === 'Enter') submit(); }} + onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }} />
@@ -51,7 +51,7 @@ export function TaskDialog({ onClose }: Props) {
- +
AI will fill description in background
diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts new file mode 100644 index 0000000..2687d47 --- /dev/null +++ b/frontend/src/store.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it, vi } from 'vitest'; +import * as store from './store'; + +describe('store startup and dispatch flows', () => { + it('hydrates persisted state and only respawns shell panes on startup', async () => { + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string) => { + if (method === 'state.hydrate') { + return { + workspaces: [ + { + id: 'ws-1', + name: 'Workspace', + path: '/repo', + spaces: [ + { + id: 'space-1', + workspace_id: 'ws-1', + name: 'Default', + tasks: [ + { + id: 'task-1', + space_id: 'space-1', + title: 'Persisted task', + description: 'Verify hydration', + status: 'todo', + priority: 'medium', + queue_status: 'none', + created_at: 1, + }, + ], + agents: [ + { + id: 'slot-1', + space_id: 'space-1', + provider_id: 'claude', + provider_name: 'Claude Code', + status: 'idle', + created_at: 1, + }, + ], + nodes: [ + { + id: 'shell-1', + space_id: 'space-1', + kind: 'shell', + title: 'Shell', + }, + ], + }, + ], + }, + ], + settings: {}, + }; + } + + if (method === 'pty.spawn') { + return { session_id: 'session-1' }; + } + + throw new Error(`Unexpected method: ${method}`); + }); + + await store.hydrateState(); + + expect(store.currentWorkspaceId.value).toBe('ws-1'); + expect(store.workspacePath.value).toBe('/repo'); + expect(store.activeSpaceId.value).toBe('space-1'); + expect(store.tasks.value.get('task-1')?.title).toBe('Persisted task'); + expect(store.agents.value.get('slot-1')?.providerName).toBe('Claude Code'); + expect(store.panes.value.find((pane) => pane.id === 'shell-1')).toMatchObject({ + sessionId: 'session-1', + sessionStatus: 'running', + }); + expect(ipcCall).toHaveBeenCalledWith('pty.spawn', { + cwd: '/repo', + kind: 'shell', + space_id: 'space-1', + node_id: 'shell-1', + }); + expect(ipcCall).toHaveBeenCalledTimes(2); + }); + + it('auto-dispatches queued tasks into idle slots during startup scheduling', async () => { + vi.useFakeTimers(); + + store.spaces.value = [{ id: 'space-1', name: 'Default' }]; + store.activeSpaceId.value = 'space-1'; + store.workspacePath.value = '/repo'; + store.schedulerSettings.value = { + concurrency: 1, + autoDispatch: true, + defaultAgentId: 'claude', + }; + store.panes.value = [ + { + id: 'task-1', + kind: 'task', + spaceId: 'space-1', + taskTitle: 'Queued task', + taskDescription: 'Dispatch me', + taskStatus: 'todo', + taskPriority: 'high', + }, + ]; + store.agents.value = new Map([ + ['slot-1', { + id: 'slot-1', + spaceId: 'space-1', + providerId: 'claude', + providerName: 'Claude Code', + status: 'idle', + sortOrder: 0, + createdAt: 1, + }], + ]); + + const ipcCall = vi.spyOn(store.ipc, 'call').mockImplementation(async (method: string, params?: Record) => { + if (method === 'task.assign') return null; + if (method === 'pty.spawn') return { session_id: 'session-42' }; + if (method === 'agent.update') return null; + if (method === 'pty.write') return null; + throw new Error(`Unexpected method: ${method} ${JSON.stringify(params)}`); + }); + + store.tasks.value = new Map([ + ['task-1', { + id: 'task-1', + spaceId: 'space-1', + title: 'Queued task', + description: 'Dispatch me', + status: 'todo', + priority: 'high', + queueStatus: 'queued', + queuedAt: 10, + sortOrder: 0, + createdAt: 1, + }], + ]); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + await vi.advanceTimersByTimeAsync(2500); + + expect(ipcCall).toHaveBeenCalledWith('task.assign', { task_id: 'task-1', agent_id: 'slot-1' }); + expect(ipcCall).toHaveBeenCalledWith('pty.spawn', { kind: 'agent', space_id: 'space-1', cwd: '/repo', command: 'claude' }); + expect(ipcCall).toHaveBeenCalledWith('agent.update', { id: 'slot-1', session_id: 'session-42' }); + expect(ipcCall).toHaveBeenCalledWith('pty.write', { session_id: 'session-42', data: 'Dispatch me\r' }); + expect(store.tasks.value.get('task-1')).toMatchObject({ + assignedAgentId: 'slot-1', + queueStatus: 'dispatched', + status: 'doing', + }); + expect(store.agents.value.get('slot-1')).toMatchObject({ + assignedTaskId: 'task-1', + status: 'running', + sessionId: 'session-42', + }); + expect(store.panes.value.find((pane) => pane.kind === 'agent')).toMatchObject({ + embedded: true, + sessionId: 'session-42', + }); + expect(store.panes.value.find((pane) => pane.id === 'task-1')?.linkedPaneId).toBeTruthy(); + }); +}); diff --git a/frontend/src/test/helpers.ts b/frontend/src/test/helpers.ts new file mode 100644 index 0000000..2b18e34 --- /dev/null +++ b/frontend/src/test/helpers.ts @@ -0,0 +1,29 @@ +import * as store from '../store'; + +export function resetStoreState() { + store.tasks.value = new Map(); + store.agents.value = new Map(); + store.schedulerSettingsEntity.value = null; + store.schedulerSettings.value = { + concurrency: 4, + autoDispatch: true, + defaultAgentId: 'claude', + }; + store.executionHistory.value = []; + store.customAgents.value = []; + store.activeAgentId.value = 'claude'; + store.panes.value = []; + store.focusedPaneId.value = null; + store.expandedPaneId.value = null; + store.popoutPanes.value = new Map(); + store.planModeAlert.value = null; + store.notes.value = []; + store.editingNoteId.value = null; + store.spaces.value = []; + store.activeSpaceId.value = null; + store.currentWorkspaceId.value = null; + store.workspacePath.value = ''; + localStorage.clear(); + document.body.innerHTML = ''; + document.documentElement.removeAttribute('data-theme'); +} diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..9fe4e3f --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1,26 @@ +import { afterEach, beforeEach, vi } from 'vitest'; +import { resetStoreState } from './helpers'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +beforeEach(() => { + resetStoreState(); + vi.useRealTimers(); +}); + +afterEach(() => { + resetStoreState(); + vi.restoreAllMocks(); +}); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ce2a218..50ea7dd 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,4 +15,9 @@ export default defineConfig({ server: { port: 5173, }, + test: { + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + restoreMocks: true, + }, }); diff --git a/src/db.zig b/src/db.zig index 645e684..b834f22 100644 --- a/src/db.zig +++ b/src/db.zig @@ -13,6 +13,10 @@ pub const Db = struct { const db_path = try resolveDbPath(allocator); defer allocator.free(db_path); + return openPath(allocator, db_path); + } + + fn openPath(allocator: Allocator, db_path: [:0]const u8) !Db { var handle: ?*c.sqlite3 = null; if (c.sqlite3_open(db_path.ptr, &handle) != c.SQLITE_OK) { if (handle) |h| _ = c.sqlite3_close(h); @@ -858,3 +862,138 @@ fn resolveDbPath(allocator: Allocator) ![:0]u8 { // Return null-terminated copy return try allocator.dupeZ(u8, path); } + +fn uniqueTestDbPath(allocator: Allocator) ![:0]u8 { + return std.fmt.allocPrintZ( + allocator, + "/tmp/nexus-db-test-{d}.sqlite", + .{std.time.nanoTimestamp()}, + ); +} + +fn freeWorkspaceRows(allocator: Allocator, rows: []WorkspaceRow) void { + for (rows) |row| { + allocator.free(row.id); + allocator.free(row.name); + allocator.free(row.path); + if (row.active_space_id) |value| allocator.free(value); + } + allocator.free(rows); +} + +fn freeSpaceRows(allocator: Allocator, rows: []SpaceRow) void { + for (rows) |row| { + allocator.free(row.id); + allocator.free(row.workspace_id); + allocator.free(row.name); + allocator.free(row.directory_path); + if (row.label_color) |value| allocator.free(value); + } + allocator.free(rows); +} + +fn freeTaskRows(allocator: Allocator, rows: []TaskRow) void { + for (rows) |row| { + allocator.free(row.id); + allocator.free(row.space_id); + if (row.parent_task_id) |value| allocator.free(value); + allocator.free(row.title); + allocator.free(row.description); + allocator.free(row.status); + allocator.free(row.priority); + allocator.free(row.queue_status); + if (row.assigned_agent_id) |value| allocator.free(value); + if (row.node_id) |value| allocator.free(value); + } + allocator.free(rows); +} + +fn freeAgentRows(allocator: Allocator, rows: []AgentRow) void { + for (rows) |row| { + allocator.free(row.id); + allocator.free(row.space_id); + allocator.free(row.provider_id); + allocator.free(row.provider_name); + allocator.free(row.status); + if (row.session_id) |value| allocator.free(value); + if (row.assigned_task_id) |value| allocator.free(value); + if (row.prompt) |value| allocator.free(value); + if (row.node_id) |value| allocator.free(value); + } + allocator.free(rows); +} + +test "Db persists workspaces, spaces, and scheduler settings across reopen" { + const allocator = std.testing.allocator; + const db_path = try uniqueTestDbPath(allocator); + defer allocator.free(db_path); + defer std.fs.cwd().deleteFile(std.mem.sliceTo(db_path, 0)) catch {}; + + { + var db = try Db.openPath(allocator, db_path); + defer db.close(); + + try db.createWorkspace("ws-1", "Workspace", "/repo"); + try db.createSpace("space-1", "ws-1", "Default", "/repo"); + try db.setSchedulerSettings("ws-1", 3, true, "claude"); + } + + { + var reopened = try Db.openPath(allocator, db_path); + defer reopened.close(); + + const workspaces = try reopened.listWorkspaces(allocator); + defer freeWorkspaceRows(allocator, workspaces); + try std.testing.expectEqual(@as(usize, 1), workspaces.len); + try std.testing.expectEqualStrings("ws-1", workspaces[0].id); + try std.testing.expectEqualStrings("/repo", workspaces[0].path); + + const spaces = try reopened.listSpaces(allocator, "ws-1"); + defer freeSpaceRows(allocator, spaces); + try std.testing.expectEqual(@as(usize, 1), spaces.len); + try std.testing.expectEqualStrings("space-1", spaces[0].id); + try std.testing.expectEqualStrings("Default", spaces[0].name); + + const settings = (try reopened.getSchedulerSettings(allocator, "ws-1")).?; + defer { + allocator.free(settings.workspace_id); + allocator.free(settings.default_agent_id); + } + try std.testing.expectEqual(@as(i32, 3), settings.concurrency); + try std.testing.expect(settings.auto_dispatch); + try std.testing.expectEqualStrings("claude", settings.default_agent_id); + } +} + +test "Db persists task queue assignment and agent linkage" { + const allocator = std.testing.allocator; + const db_path = try uniqueTestDbPath(allocator); + defer allocator.free(db_path); + defer std.fs.cwd().deleteFile(std.mem.sliceTo(db_path, 0)) catch {}; + + var db = try Db.openPath(allocator, db_path); + defer db.close(); + + try db.createWorkspace("ws-1", "Workspace", "/repo"); + try db.createSpace("space-1", "ws-1", "Default", "/repo"); + try db.createTask("task-1", "space-1", "Test persistence", "Verify dispatch", "high", null); + try db.createAgent("slot-1", "space-1", "claude", "Claude Code"); + try db.enqueueTask("task-1"); + try db.assignTaskToAgent("task-1", "slot-1"); + + const tasks_list = try db.listTasks(allocator, "space-1", null); + defer freeTaskRows(allocator, tasks_list); + try std.testing.expectEqual(@as(usize, 1), tasks_list.len); + try std.testing.expectEqualStrings("dispatched", tasks_list[0].queue_status); + try std.testing.expectEqualStrings("doing", tasks_list[0].status); + try std.testing.expect(tasks_list[0].queued_at != null); + try std.testing.expect(tasks_list[0].dispatched_at != null); + try std.testing.expectEqualStrings("slot-1", tasks_list[0].assigned_agent_id.?); + + const agents_list = try db.listAgents(allocator, "space-1", null); + defer freeAgentRows(allocator, agents_list); + try std.testing.expectEqual(@as(usize, 1), agents_list.len); + try std.testing.expectEqualStrings("running", agents_list[0].status); + try std.testing.expectEqualStrings("task-1", agents_list[0].assigned_task_id.?); + try std.testing.expect(agents_list[0].started_at != null); +} diff --git a/src/pty.zig b/src/pty.zig index c3b495d..44be48c 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -69,9 +69,8 @@ pub fn spawn( // Ensure common user bin paths are in PATH for macOS app bundles const current_path = std.posix.getenv("PATH") orelse "/usr/bin:/bin"; const home = std.posix.getenv("HOME") orelse ""; - const extended_path = std.fmt.allocPrintSentinel(allocator, - "{s}/.local/bin:{s}/.npm-global/bin:{s}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:{s}", - .{ home, home, home, current_path }, 0) catch current_path; + const extended_path = buildExtendedPath(allocator, home, current_path) catch std.posix.exit(1); + defer allocator.free(extended_path); _ = setenv("PATH", extended_path, 1); // Source user profile to get additional env vars (ANTHROPIC_API_KEY, etc.) @@ -79,11 +78,8 @@ pub fn spawn( if (command) |cmd| { // Wrap command in login shell to get full environment const shell = std.posix.getenv("SHELL") orelse "/bin/zsh"; - const wrapped_cmd = std.fmt.allocPrintSentinel(allocator, - "source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; {s}", - .{cmd}, 0) catch { - std.posix.exit(1); - }; + const wrapped_cmd = buildWrappedCommand(allocator, cmd) catch std.posix.exit(1); + defer allocator.free(wrapped_cmd); const argv = [_:null]?[*:0]const u8{ @ptrCast(shell), "-l".ptr, @@ -96,10 +92,8 @@ pub fn spawn( std.posix.execvpeZ(@ptrCast(shell), &argv, envp) catch {}; } else { const shell = std.posix.getenv("SHELL") orelse "/bin/zsh"; - const shell_basename = std.fs.path.basename(shell); - const login_name = std.fmt.allocPrintSentinel(allocator, "-{s}", .{shell_basename}, 0) catch { - std.posix.exit(1); - }; + const login_name = buildLoginArg0(allocator, shell) catch std.posix.exit(1); + defer allocator.free(login_name); const argv = [_:null]?[*:0]const u8{ login_name.ptr, @@ -125,6 +119,24 @@ pub fn spawn( }; } +fn buildExtendedPath(allocator: std.mem.Allocator, home: []const u8, current_path: []const u8) ![:0]u8 { + return std.fmt.allocPrintSentinel(allocator, + "{s}/.local/bin:{s}/.npm-global/bin:{s}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:{s}", + .{ home, home, home, current_path }, 0); +} + +fn buildWrappedCommand(allocator: std.mem.Allocator, command: []const u8) ![:0]u8 { + return std.fmt.allocPrintSentinel(allocator, + "source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; {s}", + .{command}, + 0, + ); +} + +fn buildLoginArg0(allocator: std.mem.Allocator, shell: []const u8) ![:0]u8 { + return std.fmt.allocPrintSentinel(allocator, "-{s}", .{std.fs.path.basename(shell)}, 0); +} + extern "c" fn forkpty( master: *posix.fd_t, name: ?[*:0]u8, @@ -141,3 +153,33 @@ extern "c" fn setenv( comptime { _ = @import("std").c; } + +test "buildExtendedPath keeps inherited PATH while prepending user tool directories" { + const allocator = std.testing.allocator; + const path = try buildExtendedPath(allocator, "/Users/edward", "/usr/bin:/bin"); + defer allocator.free(path); + + try std.testing.expectEqualStrings( + "/Users/edward/.local/bin:/Users/edward/.npm-global/bin:/Users/edward/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin", + std.mem.sliceTo(path, 0), + ); +} + +test "buildWrappedCommand sources shell profiles before running the command" { + const allocator = std.testing.allocator; + const wrapped = try buildWrappedCommand(allocator, "echo $FOO"); + defer allocator.free(wrapped); + + try std.testing.expectEqualStrings( + "source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; echo $FOO", + std.mem.sliceTo(wrapped, 0), + ); +} + +test "buildLoginArg0 converts the shell basename into a login-shell argv0" { + const allocator = std.testing.allocator; + const login_arg0 = try buildLoginArg0(allocator, "/bin/zsh"); + defer allocator.free(login_arg0); + + try std.testing.expectEqualStrings("-zsh", std.mem.sliceTo(login_arg0, 0)); +} diff --git a/src/test_main.zig b/src/test_main.zig new file mode 100644 index 0000000..a42e787 --- /dev/null +++ b/src/test_main.zig @@ -0,0 +1,5 @@ +comptime { + _ = @import("db.zig"); + _ = @import("grid.zig"); + _ = @import("pty.zig"); +} diff --git a/tests/run-functional-tests.sh b/tests/run-functional-tests.sh new file mode 100755 index 0000000..fbfbc23 --- /dev/null +++ b/tests/run-functional-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +npm --prefix "$repo_root/frontend" run test:run +zig build test From c25aba0502f6aebdaf0b6a5806936541ed2bd58d Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:29:47 +0000 Subject: [PATCH 03/15] fix: harden workspace import and task responses Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 23 ++++++++++++++--------- src/ipc.zig | 8 ++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 34b6463..59ae871 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -646,8 +646,9 @@ function convertTask(raw: any): TaskEntity { } function normalizeSchedulerSettings(raw: any): SchedulerSettings { + const concurrency = Number(raw?.concurrency); return { - concurrency: Math.max(1, Number(raw?.concurrency ?? DEFAULT_SCHEDULER_SETTINGS.concurrency) || DEFAULT_SCHEDULER_SETTINGS.concurrency), + concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; @@ -1555,17 +1556,22 @@ export async function importWorkspaceFromJson(json: string): Promise { } const taskIdMap = new Map(); - const pendingTasks = [...data.tasks]; + let pendingTasks = [...data.tasks]; while (pendingTasks.length > 0) { + const deferred: TaskEntity[] = []; let progressed = false; - for (let i = 0; i < pendingTasks.length; i++) { - const task = pendingTasks[i]; + for (const task of pendingTasks) { const mappedSpaceId = spaceIdMap.get(task.spaceId) ?? activeSpaceId.value; - const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; + if (!mappedSpaceId) { + throw new Error(`Could not map imported task "${task.title}" to a space`); + } - if (task.parentTaskId && !mappedParentTaskId) continue; - if (!mappedSpaceId) continue; + const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; + if (task.parentTaskId && !mappedParentTaskId) { + deferred.push(task); + continue; + } const created = await ipc.call('task.create', { id: task.id, @@ -1588,14 +1594,13 @@ export async function importWorkspaceFromJson(json: string): Promise { }); } - pendingTasks.splice(i, 1); - i--; progressed = true; } if (!progressed) { throw new Error('Could not resolve imported task hierarchy'); } + pendingTasks = deferred; } notes.value = data.notes.map(note => ({ diff --git a/src/ipc.zig b/src/ipc.zig index 882d014..95d8e5d 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -611,9 +611,9 @@ pub const Server = struct { return; }; - var resp_buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&resp_buf); - const writer = fbs.writer(); + var response = std.ArrayList(u8).init(self.allocator); + defer response.deinit(); + const writer = response.writer(); writer.writeByte('{') catch return; writer.writeAll("\"id\":") catch return; writeJsonString(writer, task_id) catch return; @@ -631,7 +631,7 @@ pub const Server = struct { writeJsonString(writer, parent) catch return; } writer.writeByte('}') catch return; - sendResult(client, id, fbs.getWritten()); + sendResult(client, id, response.items); } fn rpcTaskUpdate(self: *Server, params: std.json.ObjectMap, id: ?std.json.Value, client: *Client) void { From 4c2a12d528398bf2f4cd78447d966362969d8117 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:29:49 +0000 Subject: [PATCH 04/15] docs: add combined test runner usage Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/0672c178-4353-4190-8e32-4de6cdfa92bf Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- README.md | 13 +++++++++++++ tests/run-functional-tests.sh | 1 + 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index 908def8..b9b722a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,19 @@ zig build run zig build -Doptimize=ReleaseFast ``` +## Testing + +```bash +# Frontend functional/unit coverage +npm --prefix frontend run test:run + +# Backend Zig coverage +zig build test + +# Run both test suites +./tests/run-functional-tests.sh +``` + ### Package as macOS App ```bash diff --git a/tests/run-functional-tests.sh b/tests/run-functional-tests.sh index fbfbc23..a02320e 100755 --- a/tests/run-functional-tests.sh +++ b/tests/run-functional-tests.sh @@ -4,4 +4,5 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" npm --prefix "$repo_root/frontend" run test:run +cd "$repo_root" zig build test From 0e4385a32781f4d393f53e8c5cee41b8c4aadeed Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:31:09 +0000 Subject: [PATCH 05/15] fix: clean up workspace export wiring Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/components/SettingsDialog.tsx | 2 +- frontend/src/components/Sidebar.tsx | 2 +- frontend/src/store.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index f538c22..546d2c8 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -67,7 +67,7 @@ export function SettingsDialog({ onClose }: Props) { const wsId = currentWorkspaceId.value; if (!wsId) { alert('No workspace loaded'); return; } try { - const json = await exportWorkspace(wsId); + const json = await exportWorkspace(); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 3f768ba..a79b314 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -47,7 +47,7 @@ export function Sidebar({ onAddAgent, onAddTask, onAddNote }: Props) { const wsId = currentWorkspaceId.value; if (!wsId) { alert('No workspace loaded'); return; } try { - const json = await exportWorkspace(wsId); + const json = await exportWorkspace(); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 59ae871..4cef7bf 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1606,15 +1606,14 @@ export async function importWorkspaceFromJson(json: string): Promise { notes.value = data.notes.map(note => ({ ...note, spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, - linkedPaneId: note.linkedPaneId ? taskIdMap.get(note.linkedPaneId) : undefined, + linkedPaneId: note.linkedPaneId, })); await hydrateState(); } // Backend-backed export (full persistence) -export async function exportWorkspace(workspaceId: string): Promise { - void workspaceId; +export async function exportWorkspace(): Promise { return exportWorkspaceToJson(); } From f7b175fa108a9e2a04f287736113e7012c93e2a4 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:32:08 +0000 Subject: [PATCH 06/15] fix: tighten scheduler import normalization Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 4cef7bf..8cf21a5 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -649,7 +649,9 @@ function normalizeSchedulerSettings(raw: any): SchedulerSettings { const concurrency = Number(raw?.concurrency); return { concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), - autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + autoDispatch: typeof raw?.auto_dispatch === 'boolean' + ? raw.auto_dispatch + : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch), defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; } @@ -1606,7 +1608,6 @@ export async function importWorkspaceFromJson(json: string): Promise { notes.value = data.notes.map(note => ({ ...note, spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, - linkedPaneId: note.linkedPaneId, })); await hydrateState(); From f80758f25793ed587c7c534a93144a322d7f8dbd Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:33:16 +0000 Subject: [PATCH 07/15] refactor: clarify scheduler fallback logic Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 8cf21a5..ddc1e6c 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -647,11 +647,13 @@ function convertTask(raw: any): TaskEntity { function normalizeSchedulerSettings(raw: any): SchedulerSettings { const concurrency = Number(raw?.concurrency); + const normalizedConcurrency = Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency; + const normalizedAutoDispatch = typeof raw?.auto_dispatch === 'boolean' + ? raw.auto_dispatch + : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch); return { - concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), - autoDispatch: typeof raw?.auto_dispatch === 'boolean' - ? raw.auto_dispatch - : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + concurrency: Math.max(1, normalizedConcurrency), + autoDispatch: normalizedAutoDispatch, defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; } @@ -1634,7 +1636,8 @@ async function dispatchTaskToSlot(taskId: string, slotId: string): Promise const task = tasks.value.get(taskId); if (!task) return; - const agentProviderId = schedulerSettings.value.defaultAgentId || slot.agentProviderId; + const defaultAgentId = schedulerSettings.value.defaultAgentId?.trim(); + const agentProviderId = defaultAgentId ? defaultAgentId : slot.agentProviderId; const prompt = task.description || task.title; // 1. Assign task to agent via backend (handles bidirectional update) From 7dd1c1d10cdd027bec795be0f44b249be8ba5fd2 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:28:21 +0000 Subject: [PATCH 08/15] fix: restore workspace bootstrap and agent dispatch behavior Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/components/TerminalPane.tsx | 7 + frontend/src/store.ts | 227 ++++++++++++++++++----- src/ipc.zig | 55 +++++- src/pty.zig | 21 ++- 4 files changed, 253 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/TerminalPane.tsx b/frontend/src/components/TerminalPane.tsx index 831c171..00805c0 100644 --- a/frontend/src/components/TerminalPane.tsx +++ b/frontend/src/components/TerminalPane.tsx @@ -37,6 +37,7 @@ const TERMINAL_THEME = { // Global registry so PTY data handler can find terminals by sessionId export const terminalRegistry = new Map(); +const terminalSnapshotRegistry = new Map(); interface Props { pane: PaneState; @@ -74,6 +75,11 @@ export function TerminalPane({ pane }: Props) { terminal.loadAddon(serializeAddon); terminal.open(container); + const snapshot = terminalSnapshotRegistry.get(pane.sessionId); + if (snapshot) { + terminal.write(snapshot); + } + termRef.current = terminal; fitRef.current = fitAddon; terminalRegistry.set(pane.sessionId, terminal); @@ -95,6 +101,7 @@ export function TerminalPane({ pane }: Props) { return () => { resizeObserver.disconnect(); + terminalSnapshotRegistry.set(pane.sessionId, serializeAddon.serialize()); terminalRegistry.delete(pane.sessionId); terminal.dispose(); }; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index bb395dc..34b6463 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -95,11 +95,13 @@ export interface SchedulerSettings { defaultAgentId: string; } -export const schedulerSettings = signal({ +const DEFAULT_SCHEDULER_SETTINGS: SchedulerSettings = { concurrency: 4, autoDispatch: true, defaultAgentId: 'claude', -}); +}; + +export const schedulerSettings = signal({ ...DEFAULT_SCHEDULER_SETTINGS }); // -- Agent Pool (derived from agents signal) -- export const agentPool = computed(() => { @@ -464,7 +466,12 @@ export const activeSpacePanes = computed(() => // Only non-embedded panes go into the tiling grid export const activeSpaceGridPanes = computed(() => - panes.value.filter(p => p.spaceId === activeSpaceId.value && !p.embedded) + panes.value.filter(p => + p.spaceId === activeSpaceId.value && + !p.embedded && + p.id !== expandedPaneId.value && + !popoutPanes.value.has(p.id) + ) ); export const activeSpaceNotes = computed(() => @@ -638,6 +645,40 @@ function convertTask(raw: any): TaskEntity { }; } +function normalizeSchedulerSettings(raw: any): SchedulerSettings { + return { + concurrency: Math.max(1, Number(raw?.concurrency ?? DEFAULT_SCHEDULER_SETTINGS.concurrency) || DEFAULT_SCHEDULER_SETTINGS.concurrency), + autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, + }; +} + +async function loadSchedulerSettings(workspaceId: string): Promise { + try { + const settings = await ipc.call('scheduler.getSettings', { workspace_id: workspaceId }); + schedulerSettings.value = normalizeSchedulerSettings(settings); + } catch { + schedulerSettings.value = { ...DEFAULT_SCHEDULER_SETTINGS }; + } +} + +async function ensureWorkspace(): Promise { + if (currentWorkspaceId.value) return currentWorkspaceId.value; + + try { + const created = await ipc.call<{ id: string }>('workspace.create', { + name: 'Default', + path: workspacePath.value, + }); + currentWorkspaceId.value = created.id; + await loadSchedulerSettings(created.id); + return created.id; + } catch (err) { + console.error('workspace.create failed:', err); + return null; + } +} + // -- Internal helpers -- let paneCounter = 0; @@ -655,7 +696,10 @@ async function spawnTerminal( command?: string, prompt?: string, ): Promise { - const space = activeSpace.value; + let space = activeSpace.value; + if (!space) { + space = await createSpace('Default'); + } if (!space) return null; // Use workspace path as cwd, default to home/.claude for agents @@ -771,7 +815,10 @@ export async function createTask( priority: 'low' | 'medium' | 'high' = 'medium', parentTaskId?: string ): Promise { - const space = activeSpace.value; + let space = activeSpace.value; + if (!space) { + space = await createSpace('Default'); + } if (!space) return null; try { @@ -971,7 +1018,7 @@ function handleAgentCompletion(sessionId: string): void { // -- Workspace Path (default working directory) -- export async function setWorkspacePath(path: string): Promise { - const wsId = currentWorkspaceId.value; + const wsId = await ensureWorkspace(); if (!wsId) return; try { await ipc.call('workspace.setPath', { workspace_id: wsId, path }); @@ -1116,10 +1163,24 @@ export function focusPane(paneId: string) { focusedPaneId.value = paneId; } -export function createSpace(name: string) { - const id = `space-${Date.now()}`; - spaces.value = [...spaces.value, { id, name }]; - activeSpaceId.value = id; +export async function createSpace(name: string, id?: string): Promise { + const workspaceId = await ensureWorkspace(); + if (!workspaceId) return null; + + try { + const created = await ipc.call<{ id: string }>('space.create', { + workspace_id: workspaceId, + name, + id, + }); + const space = { id: created.id, name }; + spaces.value = [...spaces.value, space]; + activeSpaceId.value = space.id; + return space; + } catch (err) { + console.error('space.create failed:', err); + return null; + } } export function getLinkedPane(paneId: string): PaneState | undefined { @@ -1240,6 +1301,7 @@ export async function hydrateState(): Promise { const restoredTasks = new Map(); const restoredAgents = new Map(); currentWorkspaceId.value = ws.id; + await loadSchedulerSettings(ws.id); // Restore workspace path (default cwd) workspacePath.value = ws.path || ''; @@ -1339,10 +1401,12 @@ export async function hydrateState(): Promise { } // Fall back to creating default space - createSpace('Default'); + await ensureWorkspace(); + await createSpace('Default'); } catch (err) { console.error('hydrateState failed:', err); - createSpace('Default'); + await ensureWorkspace(); + await createSpace('Default'); } } @@ -1422,6 +1486,34 @@ export function exportWorkspaceToJson(): string { return JSON.stringify(exported, null, 2); } +function normalizeImportedTask(task: TaskEntity) { + if (task.status === 'done' || task.queueStatus === 'completed') { + return { status: 'done' as const, queueStatus: 'completed' as const }; + } + + if (task.queueStatus === 'queued' || task.queueStatus === 'dispatched' || task.status === 'doing') { + return { status: 'todo' as const, queueStatus: 'queued' as const }; + } + + return { status: 'todo' as const, queueStatus: 'none' as const }; +} + +async function clearCurrentWorkspaceState(): Promise { + const existingSpaces = [...spaces.value]; + for (const space of existingSpaces) { + await ipc.call('space.delete', { id: space.id }); + } + + spaces.value = []; + panes.value = []; + tasks.value = new Map(); + agents.value = new Map(); + notes.value = []; + focusedPaneId.value = null; + expandedPaneId.value = null; + popoutPanes.value = new Map(); +} + // Import workspace state from JSON (frontend-only) export async function importWorkspaceFromJson(json: string): Promise { const data = JSON.parse(json) as ExportedWorkspace; @@ -1430,60 +1522,99 @@ export async function importWorkspaceFromJson(json: string): Promise { throw new Error(`Unsupported export version: ${data.version}`); } - // Clear current state - const spaceId = activeSpaceId.value; - if (!spaceId) return; + const workspaceId = await ensureWorkspace(); + if (!workspaceId) { + throw new Error('No workspace available'); + } + + await clearCurrentWorkspaceState(); + await setWorkspacePath(data.workspace.path ?? ''); + + const importedSettings = normalizeSchedulerSettings(data.schedulerSettings); + await ipc.call('scheduler.setSettings', { + workspace_id: workspaceId, + concurrency: importedSettings.concurrency, + auto_dispatch: importedSettings.autoDispatch, + default_agent_id: importedSettings.defaultAgentId, + }); + schedulerSettings.value = importedSettings; + + const spaceIdMap = new Map(); + for (const space of data.spaces) { + const created = await createSpace(space.name, space.id); + if (created) { + spaceIdMap.set(space.id, created.id); + } + } + + if (spaceIdMap.size === 0) { + const fallback = await createSpace('Default'); + if (fallback) { + spaceIdMap.set('default', fallback.id); + } + } + + const taskIdMap = new Map(); + const pendingTasks = [...data.tasks]; + while (pendingTasks.length > 0) { + let progressed = false; + + for (let i = 0; i < pendingTasks.length; i++) { + const task = pendingTasks[i]; + const mappedSpaceId = spaceIdMap.get(task.spaceId) ?? activeSpaceId.value; + const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; - // Import tasks - for (const task of data.tasks) { - // Create task in backend - try { - await ipc.call('task.create', { + if (task.parentTaskId && !mappedParentTaskId) continue; + if (!mappedSpaceId) continue; + + const created = await ipc.call('task.create', { id: task.id, - space_id: spaceId, + space_id: mappedSpaceId, title: task.title, description: task.description, - status: task.status, priority: task.priority, - queue_status: task.queueStatus, - parent_task_id: task.parentTaskId, + parent_task_id: mappedParentTaskId, }); - } catch (e) { - console.warn('Failed to import task:', task.id, e); + + const createdId = created.id ?? task.id; + taskIdMap.set(task.id, createdId); + + const normalized = normalizeImportedTask(task); + if (normalized.status !== 'todo' || normalized.queueStatus !== 'none') { + await ipc.call('task.update', { + id: createdId, + status: normalized.status, + queue_status: normalized.queueStatus, + }); + } + + pendingTasks.splice(i, 1); + i--; + progressed = true; } - } - // Import notes - for (const note of data.notes) { - createNote(note.text); + if (!progressed) { + throw new Error('Could not resolve imported task hierarchy'); + } } - // Update scheduler settings - schedulerSettings.value = data.schedulerSettings; + notes.value = data.notes.map(note => ({ + ...note, + spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, + linkedPaneId: note.linkedPaneId ? taskIdMap.get(note.linkedPaneId) : undefined, + })); - // Re-hydrate to sync with backend await hydrateState(); } // Backend-backed export (full persistence) export async function exportWorkspace(workspaceId: string): Promise { - try { - const result = await ipc.call<{ json: string }>('workspace.export', { workspace_id: workspaceId }); - return result.json; - } catch (err) { - console.error('exportWorkspace failed:', err); - throw err; - } + void workspaceId; + return exportWorkspaceToJson(); } export async function importWorkspace(json: string): Promise { - try { - await ipc.call('workspace.import', { json }); - await hydrateState(); - } catch (err) { - console.error('importWorkspace failed:', err); - throw err; - } + await importWorkspaceFromJson(json); } // -- Auto dispatch effect (Pool-based, now with backend persistence) -- @@ -1498,7 +1629,7 @@ async function dispatchTaskToSlot(taskId: string, slotId: string): Promise const task = tasks.value.get(taskId); if (!task) return; - const agentProviderId = slot.agentProviderId; + const agentProviderId = schedulerSettings.value.defaultAgentId || slot.agentProviderId; const prompt = task.description || task.title; // 1. Assign task to agent via backend (handles bidirectional update) diff --git a/src/ipc.zig b/src/ipc.zig index 45f4b7c..882d014 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -440,9 +440,13 @@ pub const Server = struct { const workspace_id = getStr(params, "workspace_id") orelse return; const name = getStr(params, "name") orelse "Space"; const dir_path = getStr(params, "directory_path") orelse ""; + const requested_id = getStr(params, "id"); - const space_id = std.fmt.allocPrint(self.allocator, "sp-{d}", .{std.time.timestamp()}) catch return; - defer self.allocator.free(space_id); + const space_id = requested_id orelse blk: { + const generated = std.fmt.allocPrint(self.allocator, "sp-{d}", .{std.time.timestamp()}) catch return; + break :blk generated; + }; + defer if (requested_id == null) self.allocator.free(space_id); self.db.createSpace(space_id, workspace_id, name, dir_path) catch |err| { sendError(client, id, -32000, @errorName(err)); @@ -594,18 +598,40 @@ pub const Server = struct { const description = getStr(params, "description") orelse ""; const priority = getStr(params, "priority") orelse "medium"; const parent_task_id = getStr(params, "parent_task_id"); + const requested_id = getStr(params, "id"); - const task_id = std.fmt.allocPrint(self.allocator, "task-{d}", .{std.time.timestamp()}) catch return; - defer self.allocator.free(task_id); + const task_id = requested_id orelse blk: { + const generated = std.fmt.allocPrint(self.allocator, "task-{d}", .{std.time.timestamp()}) catch return; + break :blk generated; + }; + defer if (requested_id == null) self.allocator.free(task_id); self.db.createTask(task_id, space_id, title, description, priority, parent_task_id) catch |err| { sendError(client, id, -32000, @errorName(err)); return; }; - var resp_buf: [512]u8 = undefined; - const resp = std.fmt.bufPrint(&resp_buf, "{{\"id\":\"{s}\",\"space_id\":\"{s}\",\"title\":\"{s}\",\"status\":\"todo\",\"priority\":\"{s}\",\"queue_status\":\"none\"}}", .{ task_id, space_id, title, priority }) catch return; - sendResult(client, id, resp); + var resp_buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&resp_buf); + const writer = fbs.writer(); + writer.writeByte('{') catch return; + writer.writeAll("\"id\":") catch return; + writeJsonString(writer, task_id) catch return; + writer.writeAll(",\"space_id\":") catch return; + writeJsonString(writer, space_id) catch return; + writer.writeAll(",\"title\":") catch return; + writeJsonString(writer, title) catch return; + writer.writeAll(",\"description\":") catch return; + writeJsonString(writer, description) catch return; + writer.writeAll(",\"status\":\"todo\",\"priority\":") catch return; + writeJsonString(writer, priority) catch return; + writer.writeAll(",\"queue_status\":\"none\"") catch return; + if (parent_task_id) |parent| { + writer.writeAll(",\"parent_task_id\":") catch return; + writeJsonString(writer, parent) catch return; + } + writer.writeByte('}') catch return; + sendResult(client, id, fbs.getWritten()); } fn rpcTaskUpdate(self: *Server, params: std.json.ObjectMap, id: ?std.json.Value, client: *Client) void { @@ -1533,6 +1559,21 @@ fn mimeType(path: []const u8) []const u8 { return "application/octet-stream"; } +fn writeJsonString(writer: anytype, value: []const u8) !void { + try writer.writeByte('"'); + for (value) |ch| { + switch (ch) { + '"' => try writer.writeAll("\\\""), + '\\' => try writer.writeAll("\\\\"), + '\n' => try writer.writeAll("\\n"), + '\r' => try writer.writeAll("\\r"), + '\t' => try writer.writeAll("\\t"), + else => try writer.writeByte(ch), + } + } + try writer.writeByte('"'); +} + fn sendResult(client: *Client, id: ?std.json.Value, result: []const u8) void { var buf: [32768]u8 = undefined; var id_buf: [32]u8 = undefined; diff --git a/src/pty.zig b/src/pty.zig index 44be48c..7289074 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -60,7 +60,9 @@ pub fn spawn( if (pid == 0) { // Child process if (cwd) |dir| { - std.posix.chdir(dir) catch {}; + const resolved_dir = resolveWorkingDirectory(allocator, dir) catch dir; + defer if (resolved_dir.ptr != dir.ptr) allocator.free(resolved_dir); + std.posix.chdir(resolved_dir) catch {}; } // Set environment variables - these modify the process environment @@ -82,7 +84,7 @@ pub fn spawn( defer allocator.free(wrapped_cmd); const argv = [_:null]?[*:0]const u8{ @ptrCast(shell), - "-l".ptr, + "-il".ptr, "-c".ptr, wrapped_cmd.ptr, null, @@ -137,6 +139,21 @@ fn buildLoginArg0(allocator: std.mem.Allocator, shell: []const u8) ![:0]u8 { return std.fmt.allocPrintSentinel(allocator, "-{s}", .{std.fs.path.basename(shell)}, 0); } +fn resolveWorkingDirectory(allocator: std.mem.Allocator, dir: []const u8) ![]const u8 { + if (dir.len == 0 or dir[0] != '~') { + return dir; + } + + const home = std.posix.getenv("HOME") orelse return dir; + if (std.mem.eql(u8, dir, "~")) { + return allocator.dupe(u8, home); + } + if (dir.len > 1 and dir[1] == '/') { + return std.fmt.allocPrint(allocator, "{s}{s}", .{ home, dir[1..] }); + } + return dir; +} + extern "c" fn forkpty( master: *posix.fd_t, name: ?[*:0]u8, From bf982c7a4b51d4254ce7290dd5835a96a94eebc6 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:29:47 +0000 Subject: [PATCH 09/15] fix: harden workspace import and task responses Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 23 ++++++++++++++--------- src/ipc.zig | 8 ++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 34b6463..59ae871 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -646,8 +646,9 @@ function convertTask(raw: any): TaskEntity { } function normalizeSchedulerSettings(raw: any): SchedulerSettings { + const concurrency = Number(raw?.concurrency); return { - concurrency: Math.max(1, Number(raw?.concurrency ?? DEFAULT_SCHEDULER_SETTINGS.concurrency) || DEFAULT_SCHEDULER_SETTINGS.concurrency), + concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; @@ -1555,17 +1556,22 @@ export async function importWorkspaceFromJson(json: string): Promise { } const taskIdMap = new Map(); - const pendingTasks = [...data.tasks]; + let pendingTasks = [...data.tasks]; while (pendingTasks.length > 0) { + const deferred: TaskEntity[] = []; let progressed = false; - for (let i = 0; i < pendingTasks.length; i++) { - const task = pendingTasks[i]; + for (const task of pendingTasks) { const mappedSpaceId = spaceIdMap.get(task.spaceId) ?? activeSpaceId.value; - const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; + if (!mappedSpaceId) { + throw new Error(`Could not map imported task "${task.title}" to a space`); + } - if (task.parentTaskId && !mappedParentTaskId) continue; - if (!mappedSpaceId) continue; + const mappedParentTaskId = task.parentTaskId ? taskIdMap.get(task.parentTaskId) : undefined; + if (task.parentTaskId && !mappedParentTaskId) { + deferred.push(task); + continue; + } const created = await ipc.call('task.create', { id: task.id, @@ -1588,14 +1594,13 @@ export async function importWorkspaceFromJson(json: string): Promise { }); } - pendingTasks.splice(i, 1); - i--; progressed = true; } if (!progressed) { throw new Error('Could not resolve imported task hierarchy'); } + pendingTasks = deferred; } notes.value = data.notes.map(note => ({ diff --git a/src/ipc.zig b/src/ipc.zig index 882d014..95d8e5d 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -611,9 +611,9 @@ pub const Server = struct { return; }; - var resp_buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&resp_buf); - const writer = fbs.writer(); + var response = std.ArrayList(u8).init(self.allocator); + defer response.deinit(); + const writer = response.writer(); writer.writeByte('{') catch return; writer.writeAll("\"id\":") catch return; writeJsonString(writer, task_id) catch return; @@ -631,7 +631,7 @@ pub const Server = struct { writeJsonString(writer, parent) catch return; } writer.writeByte('}') catch return; - sendResult(client, id, fbs.getWritten()); + sendResult(client, id, response.items); } fn rpcTaskUpdate(self: *Server, params: std.json.ObjectMap, id: ?std.json.Value, client: *Client) void { From 7d6c1d38a8ad0aff8ea67e9171a3e07fd15528f1 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:31:09 +0000 Subject: [PATCH 10/15] fix: clean up workspace export wiring Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/components/SettingsDialog.tsx | 2 +- frontend/src/components/Sidebar.tsx | 2 +- frontend/src/store.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index f538c22..546d2c8 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -67,7 +67,7 @@ export function SettingsDialog({ onClose }: Props) { const wsId = currentWorkspaceId.value; if (!wsId) { alert('No workspace loaded'); return; } try { - const json = await exportWorkspace(wsId); + const json = await exportWorkspace(); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 3f768ba..a79b314 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -47,7 +47,7 @@ export function Sidebar({ onAddAgent, onAddTask, onAddNote }: Props) { const wsId = currentWorkspaceId.value; if (!wsId) { alert('No workspace loaded'); return; } try { - const json = await exportWorkspace(wsId); + const json = await exportWorkspace(); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 59ae871..4cef7bf 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1606,15 +1606,14 @@ export async function importWorkspaceFromJson(json: string): Promise { notes.value = data.notes.map(note => ({ ...note, spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, - linkedPaneId: note.linkedPaneId ? taskIdMap.get(note.linkedPaneId) : undefined, + linkedPaneId: note.linkedPaneId, })); await hydrateState(); } // Backend-backed export (full persistence) -export async function exportWorkspace(workspaceId: string): Promise { - void workspaceId; +export async function exportWorkspace(): Promise { return exportWorkspaceToJson(); } From c7fbbd2f794928993472b67ee22b629517632263 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:32:08 +0000 Subject: [PATCH 11/15] fix: tighten scheduler import normalization Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 4cef7bf..8cf21a5 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -649,7 +649,9 @@ function normalizeSchedulerSettings(raw: any): SchedulerSettings { const concurrency = Number(raw?.concurrency); return { concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), - autoDispatch: typeof raw?.auto_dispatch === 'boolean' ? raw.auto_dispatch : (raw?.autoDispatch ?? DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + autoDispatch: typeof raw?.auto_dispatch === 'boolean' + ? raw.auto_dispatch + : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch), defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; } @@ -1606,7 +1608,6 @@ export async function importWorkspaceFromJson(json: string): Promise { notes.value = data.notes.map(note => ({ ...note, spaceId: spaceIdMap.get(note.spaceId) ?? activeSpaceId.value ?? note.spaceId, - linkedPaneId: note.linkedPaneId, })); await hydrateState(); From 90db9acc0edbd49546238d611f722fc916d70904 Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:33:16 +0000 Subject: [PATCH 12/15] refactor: clarify scheduler fallback logic Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/43bb3c2e-14d7-4da3-b88c-9193b0d5c525 Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 8cf21a5..ddc1e6c 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -647,11 +647,13 @@ function convertTask(raw: any): TaskEntity { function normalizeSchedulerSettings(raw: any): SchedulerSettings { const concurrency = Number(raw?.concurrency); + const normalizedConcurrency = Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency; + const normalizedAutoDispatch = typeof raw?.auto_dispatch === 'boolean' + ? raw.auto_dispatch + : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch); return { - concurrency: Math.max(1, Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency), - autoDispatch: typeof raw?.auto_dispatch === 'boolean' - ? raw.auto_dispatch - : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch), + concurrency: Math.max(1, normalizedConcurrency), + autoDispatch: normalizedAutoDispatch, defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; } @@ -1634,7 +1636,8 @@ async function dispatchTaskToSlot(taskId: string, slotId: string): Promise const task = tasks.value.get(taskId); if (!task) return; - const agentProviderId = schedulerSettings.value.defaultAgentId || slot.agentProviderId; + const defaultAgentId = schedulerSettings.value.defaultAgentId?.trim(); + const agentProviderId = defaultAgentId ? defaultAgentId : slot.agentProviderId; const prompt = task.description || task.title; // 1. Assign task to agent via backend (handles bidirectional update) From 98d5e63459088b8c58321e7580dccdccfd3fdc68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:53:51 +0000 Subject: [PATCH 13/15] fix: restore test compatibility after rebase Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/16698130-31ae-4d0c-87da-807e06893fea Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/app.tsx | 11 ++++++---- frontend/src/store.ts | 51 +++++++++++++++++++++++++++++++++---------- src/ipc.zig | 8 +++---- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 7a2259d..b9ee140 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -14,10 +14,10 @@ import { PopoutContainer } from './components/PopoutPane'; import { terminalRegistry } from './components/TerminalPane'; import { ipc, hydrateState, markSessionExited, updatePane, - activeSpace, focusedPaneId, panes, deletePane, - initializeAgentPool, schedulerSettings, detectPlanMode, - loadCustomAgents, loadExecutionHistory, popoutPane, - planModeAlert, expandPane, loadPopoutPositions, + activeSpace, focusedPaneId, panes, deletePane, + initializeAgentPool, schedulerSettings, detectPlanMode, + loadCustomAgents, loadExecutionHistory, popoutPane, + planModeAlert, expandPane, loadPopoutPositions, currentWorkspaceId, loadSchedulerSettings, } from './store'; import type { SpaceState } from './store'; @@ -79,6 +79,9 @@ export function App() { // Await hydration so the active space is set before initializing the agent pool await hydrateState(); + if (currentWorkspaceId.value) { + await loadSchedulerSettings(currentWorkspaceId.value); + } // Initialize agent pool after space is known await initializeAgentPool(schedulerSettings.peek().concurrency); loadCustomAgents(); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index ddc1e6c..cba87d1 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -658,7 +658,7 @@ function normalizeSchedulerSettings(raw: any): SchedulerSettings { }; } -async function loadSchedulerSettings(workspaceId: string): Promise { +export async function loadSchedulerSettings(workspaceId: string): Promise { try { const settings = await ipc.call('scheduler.getSettings', { workspace_id: workspaceId }); schedulerSettings.value = normalizeSchedulerSettings(settings); @@ -1169,22 +1169,35 @@ export function focusPane(paneId: string) { } export async function createSpace(name: string, id?: string): Promise { - const workspaceId = await ensureWorkspace(); + const workspaceId = currentWorkspaceId.value ?? await ensureWorkspace(); if (!workspaceId) return null; + const optimisticId = id ?? `space-${Date.now()}`; + const optimisticSpace = { id: optimisticId, name }; + spaces.value = [...spaces.value, optimisticSpace]; + activeSpaceId.value = optimisticSpace.id; + try { const created = await ipc.call<{ id: string }>('space.create', { workspace_id: workspaceId, name, - id, + id: optimisticId, }); - const space = { id: created.id, name }; - spaces.value = [...spaces.value, space]; - activeSpaceId.value = space.id; - return space; + + if (created.id !== optimisticId) { + spaces.value = spaces.value.map(space => + space.id === optimisticId ? { ...space, id: created.id } : space + ); + if (activeSpaceId.value === optimisticId) { + activeSpaceId.value = created.id; + } + return { id: created.id, name }; + } + + return optimisticSpace; } catch (err) { console.error('space.create failed:', err); - return null; + return optimisticSpace; } } @@ -1306,7 +1319,6 @@ export async function hydrateState(): Promise { const restoredTasks = new Map(); const restoredAgents = new Map(); currentWorkspaceId.value = ws.id; - await loadSchedulerSettings(ws.id); // Restore workspace path (default cwd) workspacePath.value = ws.path || ''; @@ -1616,12 +1628,29 @@ export async function importWorkspaceFromJson(json: string): Promise { } // Backend-backed export (full persistence) -export async function exportWorkspace(): Promise { +export async function exportWorkspace(workspaceId = currentWorkspaceId.value ?? undefined): Promise { + if (workspaceId) { + try { + const result = await ipc.call<{ json: string }>('workspace.export', { workspace_id: workspaceId }); + return result.json; + } catch (err) { + console.warn('workspace.export failed, falling back to frontend export:', err); + } + } return exportWorkspaceToJson(); } export async function importWorkspace(json: string): Promise { - await importWorkspaceFromJson(json); + try { + const parsed = JSON.parse(json) as { version?: number }; + if (parsed.version === 1) { + await importWorkspaceFromJson(json); + return; + } + } catch {} + + await ipc.call('workspace.import', { json }); + await hydrateState(); } // -- Auto dispatch effect (Pool-based, now with backend persistence) -- diff --git a/src/ipc.zig b/src/ipc.zig index 95d8e5d..c2b4ac7 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -611,9 +611,9 @@ pub const Server = struct { return; }; - var response = std.ArrayList(u8).init(self.allocator); - defer response.deinit(); - const writer = response.writer(); + var resp_buf: [4096]u8 = undefined; + var fbs = std.io.fixedBufferStream(&resp_buf); + const writer = fbs.writer(); writer.writeByte('{') catch return; writer.writeAll("\"id\":") catch return; writeJsonString(writer, task_id) catch return; @@ -631,7 +631,7 @@ pub const Server = struct { writeJsonString(writer, parent) catch return; } writer.writeByte('}') catch return; - sendResult(client, id, response.items); + sendResult(client, id, fbs.getWritten()); } fn rpcTaskUpdate(self: *Server, params: std.json.ObjectMap, id: ?std.json.Value, client: *Client) void { From 7b1d64badac00680bdabd0d214c014c6fe84fa49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:56:22 +0000 Subject: [PATCH 14/15] fix: address rebase build and test regressions Agent-Logs-Url: https://github.com/Edward-lyz/nexus-workspace/sessions/16698130-31ae-4d0c-87da-807e06893fea Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 3 +-- src/ipc.zig | 1 + src/pty.zig | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index cba87d1..d7bc3a3 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1665,8 +1665,7 @@ async function dispatchTaskToSlot(taskId: string, slotId: string): Promise const task = tasks.value.get(taskId); if (!task) return; - const defaultAgentId = schedulerSettings.value.defaultAgentId?.trim(); - const agentProviderId = defaultAgentId ? defaultAgentId : slot.agentProviderId; + const agentProviderId = schedulerSettings.value.defaultAgentId?.trim() || slot.agentProviderId; const prompt = task.description || task.title; // 1. Assign task to agent via backend (handles bidirectional update) diff --git a/src/ipc.zig b/src/ipc.zig index c2b4ac7..a9b52aa 100644 --- a/src/ipc.zig +++ b/src/ipc.zig @@ -429,6 +429,7 @@ pub const Server = struct { }; defer self.allocator.free(path); + // Leave enough room for escaped task titles/descriptions in the RPC response payload. var resp_buf: [4096]u8 = undefined; const resp = std.fmt.bufPrint(&resp_buf, "\"{s}\"", .{path}) catch return; sendResult(client, id, resp); diff --git a/src/pty.zig b/src/pty.zig index 7289074..c16bf3c 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -84,6 +84,7 @@ pub fn spawn( defer allocator.free(wrapped_cmd); const argv = [_:null]?[*:0]const u8{ @ptrCast(shell), + // Use both login and interactive modes so shell profile files populate CLI auth env vars. "-il".ptr, "-c".ptr, wrapped_cmd.ptr, From 6cd6da6ac22e00181a1aefd2b6e4143bcbc9a30b Mon Sep 17 00:00:00 2001 From: Edward <65755881+Edward-lyz@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:59:23 +0000 Subject: [PATCH 15/15] fix: tighten rebase follow-up edge cases Co-authored-by: Edward-lyz <65755881+Edward-lyz@users.noreply.github.com> --- frontend/src/store.ts | 13 +++++++------ src/pty.zig | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index d7bc3a3..7ed7828 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -646,15 +646,16 @@ function convertTask(raw: any): TaskEntity { } function normalizeSchedulerSettings(raw: any): SchedulerSettings { - const concurrency = Number(raw?.concurrency); + const source = raw ?? {}; + const concurrency = Number(source.concurrency); const normalizedConcurrency = Number.isFinite(concurrency) ? concurrency : DEFAULT_SCHEDULER_SETTINGS.concurrency; - const normalizedAutoDispatch = typeof raw?.auto_dispatch === 'boolean' - ? raw.auto_dispatch - : (typeof raw?.autoDispatch === 'boolean' ? raw.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch); + const normalizedAutoDispatch = typeof source.auto_dispatch === 'boolean' + ? source.auto_dispatch + : (typeof source.autoDispatch === 'boolean' ? source.autoDispatch : DEFAULT_SCHEDULER_SETTINGS.autoDispatch); return { concurrency: Math.max(1, normalizedConcurrency), autoDispatch: normalizedAutoDispatch, - defaultAgentId: raw?.default_agent_id ?? raw?.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, + defaultAgentId: source.default_agent_id ?? source.defaultAgentId ?? DEFAULT_SCHEDULER_SETTINGS.defaultAgentId, }; } @@ -1614,7 +1615,7 @@ export async function importWorkspaceFromJson(json: string): Promise { } if (!progressed) { - throw new Error('Could not resolve imported task hierarchy'); + throw new Error('Could not resolve imported task hierarchy because one or more parent task references are missing'); } pendingTasks = deferred; } diff --git a/src/pty.zig b/src/pty.zig index c16bf3c..a4eb08c 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -150,7 +150,10 @@ fn resolveWorkingDirectory(allocator: std.mem.Allocator, dir: []const u8) ![]con return allocator.dupe(u8, home); } if (dir.len > 1 and dir[1] == '/') { - return std.fmt.allocPrint(allocator, "{s}{s}", .{ home, dir[1..] }); + if (dir.len == 2) { + return allocator.dupe(u8, home); + } + return std.fmt.allocPrint(allocator, "{s}/{s}", .{ home, dir[2..] }); } return dir; }