mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): clean up desktop and highlight packages
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<DesktopDaemonStatus> {
|
||||
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<DesktopDaemonStatus> {
|
||||
const current = await resolveStatus();
|
||||
logDesktopDaemonLifecycle("initial status check before start", {
|
||||
@@ -249,12 +290,10 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
|
||||
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<DesktopDaemonStatus> {
|
||||
});
|
||||
|
||||
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<DesktopDaemonStatus> {
|
||||
|
||||
@@ -142,19 +142,11 @@ export async function garbageCollectManagedAttachmentFiles(input: {
|
||||
: new Set<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,25 +280,27 @@ export async function installSkills(): Promise<InstallStatus> {
|
||||
|
||||
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<InstallStatus> {
|
||||
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) };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export function highlightCode(code: string, filename: string): HighlightToken[][
|
||||
}
|
||||
|
||||
// Build a map of character positions to styles
|
||||
const styleMap: Array<HighlightStyle | null> = new Array(code.length).fill(null);
|
||||
const styleMap: Array<HighlightStyle | null> = Array.from({ length: code.length }, () => null);
|
||||
|
||||
highlightTree(tree, highlighter, (from, to, classes) => {
|
||||
for (let i = from; i < to && i < styleMap.length; i++) {
|
||||
|
||||
Reference in New Issue
Block a user