diff --git a/packages/desktop/scripts/verify-electron-cdp.mjs b/packages/desktop/scripts/verify-electron-cdp.mjs index 15165640f..fd86ec998 100644 --- a/packages/desktop/scripts/verify-electron-cdp.mjs +++ b/packages/desktop/scripts/verify-electron-cdp.mjs @@ -144,25 +144,13 @@ async function inspectTitlebarRegions(page) { }; } - const dragSummaries = []; - const suspiciousDragHosts = []; - - for (const node of nodes) { - if (!(node instanceof HTMLElement) || !isVisible(node)) { - continue; - } - const summary = summarizeElement(node); - if (summary.appRegion !== "drag") { - continue; - } - + function buildDragRecord(node, summary) { const parent = node.parentElement instanceof HTMLElement ? node.parentElement : null; const parentSummary = parent ? summarizeElement(parent) : null; const interactiveDescendants = Array.from(node.querySelectorAll(interactiveSelector)) .filter((child) => child instanceof HTMLElement) .filter((child) => isVisible(child)) .map((child) => summarizeInteractive(child)); - const siblingResizers = parent ? Array.from(parent.children) .filter((child) => child !== node) @@ -170,7 +158,6 @@ async function inspectTitlebarRegions(page) { .filter((child) => isTopResizer(child, summary)) .map((child) => summarizeElement(child)) : []; - const parentInteractive = parent ? Array.from(parent.querySelectorAll(interactiveSelector)) .filter((child) => child instanceof HTMLElement) @@ -180,7 +167,6 @@ async function inspectTitlebarRegions(page) { const explicitNoDragInteractive = parentInteractive.filter( (child) => child.appRegion === "no-drag", ); - const record = { ...summary, parent: parentSummary, @@ -189,17 +175,25 @@ async function inspectTitlebarRegions(page) { explicitNoDragInteractive: explicitNoDragInteractive.slice(0, 5), parentInteractiveCount: parentInteractive.length, }; - dragSummaries.push(record); - const looksLikeHostShortcut = isNearTop(summary) && (summary.position !== "absolute" || summary.text.length > 0 || interactiveDescendants.length > 0 || parentSummary?.appRegion === "drag"); - if (looksLikeHostShortcut) { - suspiciousDragHosts.push(record); - } + return { record, looksLikeHostShortcut }; + } + + const dragSummaries = []; + const suspiciousDragHosts = []; + + for (const node of nodes) { + if (!(node instanceof HTMLElement) || !isVisible(node)) continue; + const summary = summarizeElement(node); + if (summary.appRegion !== "drag") continue; + const { record, looksLikeHostShortcut } = buildDragRecord(node, summary); + dragSummaries.push(record); + if (looksLikeHostShortcut) suspiciousDragHosts.push(record); } const verifiedRegions = dragSummaries @@ -230,24 +224,27 @@ async function inspectTitlebarRegions(page) { Math.abs(summary.height - candidate.height) <= 1 ); }); + function annotateMatchingParent(parent) { + if (!(parent instanceof HTMLElement)) return; + const resizers = Array.from(parent.children).filter( + (child) => child instanceof HTMLElement && isTopResizer(child, candidate), + ); + for (const child of resizers) { + child.setAttribute("data-electron-verify-resizer", "true"); + } + const interactiveChildren = Array.from(parent.querySelectorAll(interactiveSelector)) + .filter((child) => child instanceof HTMLElement) + .filter((child) => isVisible(child)) + .filter((child) => summarizeElement(child).appRegion === "no-drag") + .slice(0, 3); + for (const child of interactiveChildren) { + child.setAttribute("data-electron-verify-interactive", "true"); + } + } + if (matchingDragNode instanceof HTMLElement) { matchingDragNode.setAttribute("data-electron-verify-drag", "true"); - const parent = matchingDragNode.parentElement; - if (parent instanceof HTMLElement) { - for (const child of parent.children) { - if (child instanceof HTMLElement && isTopResizer(child, candidate)) { - child.setAttribute("data-electron-verify-resizer", "true"); - } - } - const interactiveChildren = Array.from(parent.querySelectorAll(interactiveSelector)) - .filter((child) => child instanceof HTMLElement) - .filter((child) => isVisible(child)) - .filter((child) => summarizeElement(child).appRegion === "no-drag") - .slice(0, 3); - for (const child of interactiveChildren) { - child.setAttribute("data-electron-verify-interactive", "true"); - } - } + annotateMatchingParent(matchingDragNode.parentElement); } } @@ -367,37 +364,26 @@ async function findAppPage(browser) { throw new Error(`Unable to find Electron app page for ${APP_URL_FRAGMENT}`); } -async function main() { - await ensureDir(OUTPUT_DIR); - - const browser = await chromium.connectOverCDP(CDP_URL); - const page = await findAppPage(browser); - const consoleMessages = []; - const results = []; - +function attachConsoleCollector(page, consoleMessages) { page.on("console", (message) => { - consoleMessages.push({ - type: message.type(), - text: message.text(), - }); + consoleMessages.push({ type: message.type(), text: message.text() }); }); page.on("pageerror", (error) => { - consoleMessages.push({ - type: "pageerror", - text: String(error), - }); + consoleMessages.push({ type: "pageerror", text: String(error) }); }); +} +async function navigateToWelcome(page) { await page.waitForLoadState("domcontentloaded"); await page.waitForTimeout(1000); if (!page.url().endsWith("/welcome")) { await page.goto(`http://${APP_URL_FRAGMENT}/welcome`, { waitUntil: "domcontentloaded" }); await page.waitForTimeout(1000); } +} - const welcomeScreenshot = await captureScreenshot(page, "01-welcome.png"); - - const desktopDetection = await page.evaluate(() => { +async function detectDesktopBridge(page) { + return page.evaluate(() => { const bridge = window.paseoDesktop; const keys = bridge && typeof bridge === "object" ? Object.keys(bridge) : []; const keyTypes = @@ -411,6 +397,124 @@ async function main() { platform: bridge?.platform ?? null, }; }); +} + +async function navigateToSettings(page, serverId) { + await page.evaluate((nextServerId) => { + window.location.href = `/h/${nextServerId}/settings`; + }, serverId); + await page.waitForURL(new RegExp(`/h/${escapeRegExp(serverId)}/settings$`), { + timeout: 30_000, + }); + await page.getByText("Daemon management", { exact: true }).waitFor({ + timeout: 30_000, + }); +} + +async function dismissMobileSidebarIfVisible(page) { + const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]').first(); + const menuToggle = page.locator('[data-testid="menu-button"]').first(); + const bothVisible = + (await sidebarSettingsButton.isVisible().catch(() => false)) && + (await menuToggle.isVisible().catch(() => false)); + if (!bothVisible) return; + await menuToggle.click(); + await sidebarSettingsButton.waitFor({ state: "hidden", timeout: 10_000 }).catch(() => undefined); + await page.waitForTimeout(500); +} + +function evaluateDragRegionCheck(dragRegionCheck) { + return ( + dragRegionCheck.dragRegionCount > 0 && + dragRegionCheck.verifiedRegionCount > 0 && + Boolean(dragRegionCheck.candidate) && + dragRegionCheck.candidate.top < 220 && + dragRegionCheck.candidate.parent?.appRegion !== "drag" && + dragRegionCheck.candidate.siblingResizers.length > 0 && + dragRegionCheck.suspiciousDragHosts.length === 0 + ); +} + +function evaluateTrafficLightPadding(dragRegionCheck) { + if (process.platform !== "darwin") return true; + const observedPaddingLeft = dragRegionCheck.candidate?.parent?.paddingLeft ?? null; + return ( + typeof observedPaddingLeft === "number" && + observedPaddingLeft >= 78 && + observedPaddingLeft <= 110 + ); +} + +async function collectDragRegionResults(page, dragRegionCheck, dragScreenshot, results) { + results.push({ + check: "titlebar-drag-structure", + pass: evaluateDragRegionCheck(dragRegionCheck), + details: dragRegionCheck, + screenshot: dragScreenshot, + }); + + const trafficLightScreenshot = await captureScreenshot(page, "04-traffic-light-padding.png"); + results.push({ + check: "traffic-light-padding", + pass: evaluateTrafficLightPadding(dragRegionCheck), + details: { + platform: process.platform, + observedPaddingLeft: dragRegionCheck.candidate?.parent?.paddingLeft ?? null, + note: "Traffic-light padding is only validated structurally on macOS in this verifier.", + candidate: dragRegionCheck.candidate, + }, + screenshot: trafficLightScreenshot, + }); + + results.push({ + check: "interactive-no-drag-layering", + pass: + Boolean(dragRegionCheck.candidate) && + Array.isArray(dragRegionCheck.candidate.explicitNoDragInteractive) && + dragRegionCheck.candidate.explicitNoDragInteractive.length > 0, + details: { + candidate: dragRegionCheck.candidate, + explicitNoDragInteractive: dragRegionCheck.candidate?.explicitNoDragInteractive ?? [], + }, + screenshot: dragScreenshot, + }); +} + +async function collectDaemonManagementResult(page, serverId, desktopStatus, results) { + const daemonManagementVisible = await Promise.all([ + page.getByText("Built-in daemon", { exact: true }).isVisible(), + page.getByText("Daemon management", { exact: true }).isVisible(), + page.getByRole("button", { name: "Restart daemon" }).first().isVisible(), + ]).then((values) => values.every(Boolean)); + const daemonManagementScreenshot = await captureScreenshot( + page, + "05-settings-daemon-management.png", + ); + results.push({ + check: "settings-daemon-management", + pass: daemonManagementVisible, + details: { + route: page.url(), + serverId, + desktopStatus, + }, + screenshot: daemonManagementScreenshot, + }); +} + +async function main() { + await ensureDir(OUTPUT_DIR); + + const browser = await chromium.connectOverCDP(CDP_URL); + const page = await findAppPage(browser); + const consoleMessages = []; + const results = []; + + attachConsoleCollector(page, consoleMessages); + await navigateToWelcome(page); + + const welcomeScreenshot = await captureScreenshot(page, "01-welcome.png"); + const desktopDetection = await detectDesktopBridge(page); const hasExpectedDesktopShape = desktopDetection.exists && @@ -432,84 +536,14 @@ async function main() { ); const serverId = desktopStatus.serverId.trim(); - await page.evaluate((nextServerId) => { - window.location.href = `/h/${nextServerId}/settings`; - }, serverId); - await page.waitForURL(new RegExp(`/h/${escapeRegExp(serverId)}/settings$`), { - timeout: 30_000, - }); - await page.getByText("Daemon management", { exact: true }).waitFor({ - timeout: 30_000, - }); + await navigateToSettings(page, serverId); - const settingsScreenshot = await captureScreenshot(page, "02-settings-page.png"); - - const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]').first(); - const menuToggle = page.locator('[data-testid="menu-button"]').first(); - if ( - (await sidebarSettingsButton.isVisible().catch(() => false)) && - (await menuToggle.isVisible().catch(() => false)) - ) { - await menuToggle.click(); - await sidebarSettingsButton - .waitFor({ state: "hidden", timeout: 10_000 }) - .catch(() => undefined); - await page.waitForTimeout(500); - } + await captureScreenshot(page, "02-settings-page.png"); + await dismissMobileSidebarIfVisible(page); const dragRegionCheck = await inspectTitlebarRegions(page); - const dragScreenshot = await captureScreenshot(page, "03-drag-region.png"); - const dragRegionPassed = - dragRegionCheck.dragRegionCount > 0 && - dragRegionCheck.verifiedRegionCount > 0 && - Boolean(dragRegionCheck.candidate) && - dragRegionCheck.candidate.top < 220 && - dragRegionCheck.candidate.parent?.appRegion !== "drag" && - dragRegionCheck.candidate.siblingResizers.length > 0 && - dragRegionCheck.suspiciousDragHosts.length === 0; - - results.push({ - check: "titlebar-drag-structure", - pass: dragRegionPassed, - details: dragRegionCheck, - screenshot: dragScreenshot, - }); - - const trafficLightScreenshot = await captureScreenshot(page, "04-traffic-light-padding.png"); - const isMac = process.platform === "darwin"; - const observedPaddingLeft = dragRegionCheck.candidate?.parent?.paddingLeft ?? null; - const trafficLightPaddingPassed = !isMac - ? true - : typeof observedPaddingLeft === "number" && - observedPaddingLeft >= 78 && - observedPaddingLeft <= 110; - - results.push({ - check: "traffic-light-padding", - pass: trafficLightPaddingPassed, - details: { - platform: process.platform, - observedPaddingLeft, - note: "Traffic-light padding is only validated structurally on macOS in this verifier.", - candidate: dragRegionCheck.candidate, - }, - screenshot: trafficLightScreenshot, - }); - - const noDragInteractiveCheck = { - check: "interactive-no-drag-layering", - pass: - Boolean(dragRegionCheck.candidate) && - Array.isArray(dragRegionCheck.candidate.explicitNoDragInteractive) && - dragRegionCheck.candidate.explicitNoDragInteractive.length > 0, - details: { - candidate: dragRegionCheck.candidate, - explicitNoDragInteractive: dragRegionCheck.candidate?.explicitNoDragInteractive ?? [], - }, - screenshot: dragScreenshot, - }; - results.push(noDragInteractiveCheck); + await collectDragRegionResults(page, dragRegionCheck, dragScreenshot, results); const fullscreenDetails = await inspectFullscreenResizer(page); const fullscreenScreenshot = await captureScreenshot(page, "04-fullscreen-resizer.png"); @@ -520,26 +554,7 @@ async function main() { screenshot: fullscreenScreenshot, }); - const daemonManagementVisible = await Promise.all([ - page.getByText("Built-in daemon", { exact: true }).isVisible(), - page.getByText("Daemon management", { exact: true }).isVisible(), - page.getByRole("button", { name: "Restart daemon" }).first().isVisible(), - ]).then((values) => values.every(Boolean)); - const daemonManagementScreenshot = await captureScreenshot( - page, - "05-settings-daemon-management.png", - ); - - results.push({ - check: "settings-daemon-management", - pass: daemonManagementVisible, - details: { - route: page.url(), - serverId, - desktopStatus, - }, - screenshot: daemonManagementScreenshot, - }); + await collectDaemonManagementResult(page, serverId, desktopStatus, results); const desktopDetectionScreenshot = await captureScreenshot(page, "06-desktop-detection.png"); results[0].screenshot = desktopDetectionScreenshot; diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index a5ddc7460..c35b12a62 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -238,6 +238,47 @@ function normalizeVersion(version: string | null): string | null { return trimmed.replace(/^v/i, ""); } +function shouldRestartForVersion(current: DesktopDaemonStatus): boolean { + if (!current.desktopManaged) return false; + const appVersion = normalizeVersion(resolveDesktopAppVersion()); + const daemonVersion = normalizeVersion(current.version); + return Boolean(appVersion && daemonVersion && appVersion !== daemonVersion); +} + +function buildStartupFailureError( + result: { code: number | null; signal: string | null; error?: Error }, + stdout: string, + stderr: string, +): Error { + const reason = result.error + ? result.error.message + : `exit code ${result.code ?? "unknown"}${result.signal ? ` (${result.signal})` : ""}`; + const parts = [`Daemon failed to start: ${reason}`]; + if (stderr.trim()) parts.push(`stderr:\n${stderr.trim()}`); + if (stdout.trim()) parts.push(`stdout:\n${stdout.trim()}`); + const logs = tailFile(logFilePath(), 15); + if (logs) parts.push(`Recent logs (${logFilePath()}):\n${logs}`); + return new Error(parts.join("\n\n")); +} + +async function pollForRunningDaemon(): Promise { + for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) { + const status = await resolveStatus(); + if (attempt === 0 || attempt === STARTUP_POLL_MAX_ATTEMPTS - 1 || attempt % 10 === 9) { + logDesktopDaemonLifecycle("polling daemon status after detached start", { + attempt: attempt + 1, + status: status.status, + pid: status.pid, + listen: status.listen, + serverId: status.serverId || null, + }); + } + if (status.status === "running" && status.serverId && status.listen) return status; + await sleep(STARTUP_POLL_INTERVAL_MS); + } + return resolveStatus(); +} + async function startDaemon(): Promise { const current = await resolveStatus(); logDesktopDaemonLifecycle("initial status check before start", { @@ -249,12 +290,10 @@ async function startDaemon(): Promise { desktopManaged: current.desktopManaged, }); if (current.status === "running") { - const appVersion = normalizeVersion(resolveDesktopAppVersion()); - const daemonVersion = normalizeVersion(current.version); - if (current.desktopManaged && appVersion && daemonVersion && appVersion !== daemonVersion) { + if (shouldRestartForVersion(current)) { logDesktopDaemonLifecycle("daemon version mismatch, restarting", { - appVersion, - daemonVersion, + appVersion: normalizeVersion(resolveDesktopAppVersion()), + daemonVersion: normalizeVersion(current.version), }); await stopDaemon(); } else { @@ -340,34 +379,10 @@ async function startDaemon(): Promise { }); if (result.exitedEarly) { - const reason = result.error - ? result.error.message - : `exit code ${result.code ?? "unknown"}${result.signal ? ` (${result.signal})` : ""}`; - const parts = [`Daemon failed to start: ${reason}`]; - if (stderr.trim()) parts.push(`stderr:\n${stderr.trim()}`); - if (stdout.trim()) parts.push(`stdout:\n${stdout.trim()}`); - const logs = tailFile(logFilePath(), 15); - if (logs) parts.push(`Recent logs (${logFilePath()}):\n${logs}`); - throw new Error(parts.join("\n\n")); + throw buildStartupFailureError(result, stdout, stderr); } - // Poll for PID file with server ID - for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) { - const status = await resolveStatus(); - if (attempt === 0 || attempt === STARTUP_POLL_MAX_ATTEMPTS - 1 || attempt % 10 === 9) { - logDesktopDaemonLifecycle("polling daemon status after detached start", { - attempt: attempt + 1, - status: status.status, - pid: status.pid, - listen: status.listen, - serverId: status.serverId || null, - }); - } - if (status.status === "running" && status.serverId && status.listen) return status; - await sleep(STARTUP_POLL_INTERVAL_MS); - } - - return await resolveStatus(); + return pollForRunningDaemon(); } async function stopDaemon(): Promise { diff --git a/packages/desktop/src/features/attachments.ts b/packages/desktop/src/features/attachments.ts index da4336c92..5370fe435 100644 --- a/packages/desktop/src/features/attachments.ts +++ b/packages/desktop/src/features/attachments.ts @@ -142,19 +142,11 @@ export async function garbageCollectManagedAttachmentFiles(input: { : new Set(); const entries = await readdir(dirPath, { withFileTypes: true }); - let deletedCount = 0; + const toDelete = entries.filter( + (entry) => entry.isFile() && !referencedIds.has(path.parse(entry.name).name), + ); - for (const entry of entries) { - if (!entry.isFile()) { - continue; - } - const attachmentId = path.parse(entry.name).name; - if (referencedIds.has(attachmentId)) { - continue; - } - await rm(path.join(dirPath, entry.name), { force: true }); - deletedCount += 1; - } + await Promise.all(toDelete.map((entry) => rm(path.join(dirPath, entry.name), { force: true }))); - return deletedCount; + return toDelete.length; } diff --git a/packages/desktop/src/features/dialogs.ts b/packages/desktop/src/features/dialogs.ts index 4227cd3e1..d5920878a 100644 --- a/packages/desktop/src/features/dialogs.ts +++ b/packages/desktop/src/features/dialogs.ts @@ -15,12 +15,17 @@ interface OpenOptions { filters?: Array<{ name: string; extensions: string[] }>; } +function resolveDialogType(kind: AskOptions["kind"]): "warning" | "error" | "question" { + if (kind === "warning") return "warning"; + if (kind === "error") return "error"; + return "question"; +} + export function registerDialogHandlers(): void { ipcMain.handle("paseo:dialog:ask", async (event, message: string, options?: AskOptions) => { const win = BrowserWindow.fromWebContents(event.sender); const result = await dialog.showMessageBox(win ?? BrowserWindow.getFocusedWindow()!, { - type: - options?.kind === "warning" ? "warning" : options?.kind === "error" ? "error" : "question", + type: resolveDialogType(options?.kind), title: options?.title ?? "Confirm", message, buttons: [options?.cancelLabel ?? "Cancel", options?.okLabel ?? "OK"], diff --git a/packages/desktop/src/features/menu.ts b/packages/desktop/src/features/menu.ts index 71b06b536..945f70028 100644 --- a/packages/desktop/src/features/menu.ts +++ b/packages/desktop/src/features/menu.ts @@ -105,7 +105,7 @@ export function setupApplicationMenu(): void { return; } - const menu = Menu.buildFromTemplate([ + const contextMenu = Menu.buildFromTemplate([ { label: "Copy", role: "copy", @@ -124,6 +124,6 @@ export function setupApplicationMenu(): void { }, ]); - menu.popup({ window: win }); + contextMenu.popup({ window: win }); }); } diff --git a/packages/desktop/src/integrations/integrations-manager.ts b/packages/desktop/src/integrations/integrations-manager.ts index f137c8eda..58ceadb0f 100644 --- a/packages/desktop/src/integrations/integrations-manager.ts +++ b/packages/desktop/src/integrations/integrations-manager.ts @@ -280,25 +280,27 @@ export async function installSkills(): Promise { log.info("[integrations] installSkills", { sourceDir, agentsDir, claudeDir, codexDir }); - for (const skillName of SKILL_NAMES) { - const sourceFile = path.join(sourceDir, skillName, "SKILL.md"); - await copySkillFile(sourceFile, agentsDir, skillName); - await symlinkSkillDir(skillName, agentsDir, claudeDir); - await copySkillFile(sourceFile, codexDir, skillName); - } + await Promise.all( + SKILL_NAMES.map(async (skillName) => { + const sourceFile = path.join(sourceDir, skillName, "SKILL.md"); + await copySkillFile(sourceFile, agentsDir, skillName); + await symlinkSkillDir(skillName, agentsDir, claudeDir); + await copySkillFile(sourceFile, codexDir, skillName); + }), + ); return getSkillsInstallStatus(); } export async function getSkillsInstallStatus(): Promise { const claudeDir = getClaudeSkillsDir(); - for (const skillName of SKILL_NAMES) { - const skillFile = path.join(claudeDir, skillName, "SKILL.md"); - try { - await fs.access(skillFile); - } catch { - return { installed: false }; - } - } - return { installed: true }; + const accessResults = await Promise.all( + SKILL_NAMES.map((skillName) => + fs + .access(path.join(claudeDir, skillName, "SKILL.md")) + .then(() => true) + .catch(() => false), + ), + ); + return { installed: accessResults.every(Boolean) }; } diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index c57654955..b19ebc7d3 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -120,20 +120,27 @@ function getAppDistDir(): string { return path.resolve(__dirname, "../../app/dist"); } -function getWindowIconPath(): string | null { - const candidates = app.isPackaged - ? process.platform === "win32" - ? [path.join(process.resourcesPath, "icon.ico"), path.join(process.resourcesPath, "icon.png")] - : [path.join(process.resourcesPath, "icon.png")] - : process.platform === "darwin" - ? [path.resolve(__dirname, "../assets/icon.png")] - : process.platform === "win32" - ? [ - path.resolve(__dirname, "../assets/icon.ico"), - path.resolve(__dirname, "../assets/icon.png"), - ] - : [path.resolve(__dirname, "../assets/icon.png")]; +function getWindowIconCandidates(): string[] { + if (app.isPackaged) { + if (process.platform === "win32") { + return [ + path.join(process.resourcesPath, "icon.ico"), + path.join(process.resourcesPath, "icon.png"), + ]; + } + return [path.join(process.resourcesPath, "icon.png")]; + } + if (process.platform === "win32") { + return [ + path.resolve(__dirname, "../assets/icon.ico"), + path.resolve(__dirname, "../assets/icon.png"), + ]; + } + return [path.resolve(__dirname, "../assets/icon.png")]; +} +function getWindowIconPath(): string | null { + const candidates = getWindowIconCandidates(); return candidates.find((candidate) => existsSync(candidate)) ?? null; } diff --git a/packages/highlight/src/highlighter.ts b/packages/highlight/src/highlighter.ts index bd824ace8..269b7cd6a 100644 --- a/packages/highlight/src/highlighter.ts +++ b/packages/highlight/src/highlighter.ts @@ -62,7 +62,7 @@ export function highlightCode(code: string, filename: string): HighlightToken[][ } // Build a map of character positions to styles - const styleMap: Array = new Array(code.length).fill(null); + const styleMap: Array = Array.from({ length: code.length }, () => null); highlightTree(tree, highlighter, (from, to, classes) => { for (let i = from; i < to && i < styleMap.length; i++) {