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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Works with any CLI agent. Built for local worktree-based development.
| **タブカラー設定** | タブを右クリック → Set Color で13色から背景色を設定可能。ワークスペースセクションと同じカラーパレットを再利用。アクティブ/非アクティブで濃淡が変化し、設定は自動永続化 | [#12](https://github.com/MocA-Love/superset/pull/12) | 2026-03-29 |
| **クラッシュリカバリー強化** | macOS でアプリが白画面/フリーズする問題を修正。GPU クラッシュ時に最大化/フルスクリーンでもコンポジター再構築を実行、レンダラークラッシュ時の自動リロード/再起動、clipboard 操作のエラーハンドリング追加 | [#13](https://github.com/MocA-Love/superset/pull/13) | 2026-03-29 |
| **Excel 描画オブジェクト・斜線表示** | Excel ファイルの描画オブジェクト(線・矩形)とセル斜線を表示。xlsx ZIP から drawing XML を直接パースし、CSS transform 方式の SVG オーバーレイで正確に配置 | [#16](https://github.com/MocA-Love/superset/pull/16) | 2026-03-29 |
| **Excel diff インラインハイライト** | Excel 差分表示で変更セル内のテキスト差分を文字レベルでインライン表示。追加部分は緑、削除部分は赤+取り消し線。セルからはみ出る場合はホバーでツールチップにフル差分を表示 | [#19](https://github.com/MocA-Love/superset/pull/19) | 2026-03-29 |

## Fork のビルド方法 (macOS)

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"clsx": "^2.1.1",
"culori": "^4.0.2",
"date-fns": "^4.1.0",
"diff": "^7.0.0",
"default-shell": "^2.2.0",
"dnd-core": "^16.0.1",
"dotenv": "^17.3.1",
Expand Down Expand Up @@ -230,6 +231,7 @@
"@tanstack/router-cli": "^1.149.0",
"@tanstack/router-plugin": "^1.149.0",
"@types/better-sqlite3": "^7.6.13",
"@types/diff": "^6.0.0",
"@types/bun": "^1.2.17",
"@types/culori": "^4.0.1",
"@types/http-proxy": "^1.17.17",
Expand Down
34 changes: 34 additions & 0 deletions apps/desktop/src/lib/trpc/routers/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from "zod";
import { publicProcedure, router } from "../..";
import {
installExtension,
listExtensions,
toggleExtension,
uninstallExtension,
} from "main/lib/extensions/extension-manager";

export const createExtensionsRouter = () => {
return router({
list: publicProcedure.query(async () => {
return listExtensions();
}),

install: publicProcedure
.input(z.object({ input: z.string() }))
.mutation(async ({ input }) => {
return installExtension(input.input);
}),

uninstall: publicProcedure
.input(z.object({ extensionId: z.string() }))
.mutation(async ({ input }) => {
await uninstallExtension(input.extensionId);
}),

toggle: publicProcedure
.input(z.object({ extensionId: z.string(), enabled: z.boolean() }))
.mutation(async ({ input }) => {
return toggleExtension(input.extensionId, input.enabled);
}),
});
};
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { createTerminalRouter } from "./terminal";
import { createUiStateRouter } from "./ui-state";
import { createWindowRouter } from "./window";
import { createTabTearoffRouter } from "./tab-tearoff";
import { createExtensionsRouter } from "./extensions";
import { createWorkspacesRouter } from "./workspaces";

export const createAppRouter = (
Expand Down Expand Up @@ -63,6 +64,7 @@ export const createAppRouter = (
ringtone: createRingtoneRouter(getWindow),
hostServiceManager: createHostServiceManagerRouter(),
tabTearoff: createTabTearoffRouter(wm),
extensions: createExtensionsRouter(),
});
};

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { setupAutoUpdater } from "./lib/auto-updater";
import { resolveDevWorkspaceName } from "./lib/dev-workspace-name";
import { setWorkspaceDockIcon } from "./lib/dock-icon";
import { loadWebviewBrowserExtension } from "./lib/extensions";
import { loadInstalledExtensions } from "./lib/extensions/extension-manager";
import { getHostServiceManager } from "./lib/host-service-manager";
import { localDb } from "./lib/local-db";
import { ensureProjectIconsDir, getProjectIconPath } from "./lib/project-icons";
Expand Down Expand Up @@ -334,6 +335,7 @@ if (!gotTheLock) {
await initAppState();

await loadWebviewBrowserExtension();
await loadInstalledExtensions();

// Must happen before renderer restore runs
await reconcileDaemonSessions();
Expand Down
273 changes: 273 additions & 0 deletions apps/desktop/src/main/lib/extensions/compatibility-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { glob } from "fast-glob";
import type { ChromeManifest } from "./crx-downloader";

/** APIs fully supported in Electron */
const SUPPORTED_APIS = new Set([
"chrome.devtools.inspectedWindow",
"chrome.devtools.network",
"chrome.devtools.panels",
"chrome.scripting",
"chrome.webRequest",
"chrome.storage.local",
"chrome.runtime.lastError",
"chrome.runtime.id",
"chrome.runtime.getManifest",
"chrome.runtime.getURL",
"chrome.runtime.connect",
"chrome.runtime.sendMessage",
"chrome.runtime.onConnect",
"chrome.runtime.onMessage",
"chrome.runtime.onInstalled",
"chrome.runtime.onStartup",
"chrome.extension.getURL",
"chrome.extension.getBackgroundPage",
]);

/** Permissions that Electron cannot provide */
const UNSUPPORTED_PERMISSIONS = new Set([
"bookmarks",
"browsingData",
"contentSettings",
"cookies",
"debugger",
"declarativeContent",
"declarativeNetRequest",
"desktopCapture",
"downloads",
"downloads.shelf",
"enterprise.deviceAttributes",
"enterprise.platformKeys",
"fontSettings",
"gcm",
"geolocation",
"history",
"identity",
"idle",
"loginState",
"nativeMessaging",
"notifications",
"pageCapture",
"platformKeys",
"power",
"printerProvider",
"printing",
"printingMetrics",
"privacy",
"proxy",
"search",
"sessions",
"signedInDevices",
"system.cpu",
"system.display",
"system.memory",
"system.storage",
"tabCapture",
"tabGroups",
"topSites",
"tts",
"ttsEngine",
"wallpaper",
"webNavigation",
]);

/** chrome.* API patterns that don't work in Electron */
const UNSUPPORTED_API_PATTERNS = [
"chrome.bookmarks",
"chrome.browsingData",
"chrome.contentSettings",
"chrome.cookies",
"chrome.debugger",
"chrome.declarativeContent",
"chrome.declarativeNetRequest",
"chrome.desktopCapture",
"chrome.downloads",
"chrome.fontSettings",
"chrome.gcm",
"chrome.history",
"chrome.identity",
"chrome.notifications",
"chrome.pageCapture",
"chrome.privacy",
"chrome.proxy",
"chrome.sessions",
"chrome.tabCapture",
"chrome.tabGroups",
"chrome.topSites",
"chrome.tts",
"chrome.ttsEngine",
"chrome.webNavigation",
"chrome.storage.sync",
"chrome.storage.managed",
"chrome.tabs.create",
"chrome.tabs.remove",
"chrome.tabs.move",
"chrome.tabs.group",
"chrome.tabs.ungroup",
"chrome.tabs.duplicate",
"chrome.tabs.discard",
"chrome.tabs.captureVisibleTab",
"chrome.tabs.goBack",
"chrome.tabs.goForward",
"chrome.windows.create",
"chrome.windows.remove",
"chrome.windows.update",
];

export type CompatibilityLevel = "full" | "partial" | "low";

export interface CompatibilityIssue {
type: "unsupported_permission" | "unsupported_api" | "unsupported_feature";
severity: "warning" | "error";
message: string;
detail?: string;
}

export interface CompatibilityReport {
level: CompatibilityLevel;
issues: CompatibilityIssue[];
summary: string;
}

/**
* Check extension manifest for unsupported features.
*/
function checkManifest(manifest: ChromeManifest): CompatibilityIssue[] {
const issues: CompatibilityIssue[] = [];

// Check permissions
const allPermissions = [
...(manifest.permissions ?? []),
...(manifest.optional_permissions ?? []),
];

for (const perm of allPermissions) {
if (UNSUPPORTED_PERMISSIONS.has(perm)) {
issues.push({
type: "unsupported_permission",
severity: "warning",
message: `Permission "${perm}" is not supported in Electron`,
});
}
}

// Check popup UI
const action = manifest.action ?? manifest.browser_action;
if (action?.default_popup) {
issues.push({
type: "unsupported_feature",
severity: "warning",
message: "Popup UI (browser_action/action popup) is not supported",
detail:
"The extension's popup window will not be displayed in Electron",
});
}

// Check chrome_url_overrides
if (manifest.chrome_url_overrides) {
issues.push({
type: "unsupported_feature",
severity: "error",
message: "Chrome URL overrides (new tab, history, bookmarks pages) are not supported",
});
}

// Check options_ui
if (manifest.options_ui || manifest.options_page) {
issues.push({
type: "unsupported_feature",
severity: "warning",
message: "Options page may not work as expected",
detail:
"Extension options pages rely on chrome.runtime.openOptionsPage() which has limited support",
});
}

return issues;
}

/**
* Scan the extension's JS files for usage of unsupported chrome.* APIs.
*/
async function scanJsForUnsupportedApis(
extensionDir: string,
): Promise<CompatibilityIssue[]> {
const issues: CompatibilityIssue[] = [];
const seen = new Set<string>();

const jsFiles = await glob("**/*.js", {
cwd: extensionDir,
absolute: true,
ignore: ["**/node_modules/**"],
});

for (const file of jsFiles) {
let content: string;
try {
content = await readFile(file, "utf-8");
} catch {
continue;
}

for (const api of UNSUPPORTED_API_PATTERNS) {
if (seen.has(api)) continue;

// Escape dots for regex, match the API call pattern
const pattern = api.replace(/\./g, "\\.");
const regex = new RegExp(`${pattern}\\b`);

if (regex.test(content)) {
seen.add(api);
issues.push({
type: "unsupported_api",
severity: "warning",
message: `Uses "${api}" which is not supported in Electron`,
detail: `Found in ${path.basename(file)}`,
});
}
}
}

return issues;
}

/**
* Run a full compatibility check on an unpacked extension.
*/
export async function checkCompatibility(
extensionDir: string,
manifest: ChromeManifest,
): Promise<CompatibilityReport> {
const manifestIssues = checkManifest(manifest);
const apiIssues = await scanJsForUnsupportedApis(extensionDir);

const issues = [...manifestIssues, ...apiIssues];

const errorCount = issues.filter((i) => i.severity === "error").length;
const warningCount = issues.filter((i) => i.severity === "warning").length;

let level: CompatibilityLevel;
if (errorCount > 0 || warningCount >= 5) {
level = "low";
} else if (warningCount > 0) {
level = "partial";
} else {
level = "full";
}

let summary: string;
switch (level) {
case "full":
summary = "This extension is expected to work well in Electron.";
break;
case "partial":
summary = `This extension may have limited functionality (${warningCount} potential issue${warningCount > 1 ? "s" : ""}).`;
break;
case "low":
summary = `This extension is likely incompatible (${errorCount} critical, ${warningCount} warning${warningCount > 1 ? "s" : ""}).`;
break;
}

return { level, issues, summary };
}
Loading