diff --git a/README.md b/README.md index 76940b20171..8d697fe41fe 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Works with any CLI agent. Built for local worktree-based development. | **大規模ファイル diff 高速化** | 2000行超のファイルで CodeMirror 6 ベースの仮想化 diff ビューアに自動切替。ビューポート分のDOMのみ描画し、15000行でもスムーズ表示。既存テーマ・シンタックスハイライト再利用、未変更領域の自動折りたたみ | [#5](https://github.com/MocA-Love/superset/pull/5) | 2026-03-28 | | **ports.json ポートの常時表示** | ports.json に定義されたポートをプロセス検出の有無にかかわらず常にサイドバーに表示。Docker 等で検知できないポートもラベル付きで一覧に出る。検出済みポートは従来通りアクティブ表示、未検出は グレー表示で区別 | [#7](https://github.com/MocA-Love/superset/pull/7) | 2026-03-28 | | **Ports ワークスペース名の改善** | Ports セクションのワークスペース名をワークツリーのディレクトリ名ベースに変更。同名ワークスペースが複数ある場合でもどのワークツリーか一目で区別可能 | [#8](https://github.com/MocA-Love/superset/pull/8) | 2026-03-28 | +| **ブラウザタブ機能強化** | ズーム倍率表示と [-]/[+] ボタン(Cmd+/- と同期)、target="_blank" リンクや Cmd+click を新しいブラウザタブで開く機能、URL コピーボタンを追加。タブが非表示中でもリンクイベントを正しく処理するグローバルハンドラ実装 | [#10](https://github.com/MocA-Love/superset/pull/10) | 2026-03-29 | ## Fork のビルド方法 (macOS) diff --git a/apps/desktop/src/lib/trpc/routers/browser/browser.ts b/apps/desktop/src/lib/trpc/routers/browser/browser.ts index 50681573e85..f2aed8c6bc9 100644 --- a/apps/desktop/src/lib/trpc/routers/browser/browser.ts +++ b/apps/desktop/src/lib/trpc/routers/browser/browser.ts @@ -115,6 +115,19 @@ export const createBrowserRouter = () => { }); }), + /** Global subscription for new-window events from any browser pane. */ + onAnyNewWindow: publicProcedure.subscription(() => { + return observable<{ paneId: string; url: string }>((emit) => { + const handler = (data: { paneId: string; url: string }) => { + emit.next(data); + }; + browserManager.on("new-window", handler); + return () => { + browserManager.off("new-window", handler); + }; + }); + }), + onContextMenuAction: publicProcedure .input(z.object({ paneId: z.string() })) .subscription(({ input }) => { @@ -136,6 +149,33 @@ export const createBrowserRouter = () => { return { success: true }; }), + setZoomLevel: publicProcedure + .input(z.object({ paneId: z.string(), level: z.number() })) + .mutation(({ input }) => { + const wc = browserManager.getWebContents(input.paneId); + if (!wc) return { success: false }; + wc.setZoomLevel(input.level); + return { success: true }; + }), + + onZoomChanged: publicProcedure + .input(z.object({ paneId: z.string() })) + .subscription(({ input }) => { + return observable<{ zoomLevel: number }>((emit) => { + let lastLevel: number | null = null; + const interval = setInterval(() => { + const wc = browserManager.getWebContents(input.paneId); + if (!wc) return; + const level = wc.getZoomLevel(); + if (level !== lastLevel) { + lastLevel = level; + emit.next({ zoomLevel: level }); + } + }, 300); + return () => clearInterval(interval); + }); + }), + getPageInfo: publicProcedure .input(z.object({ paneId: z.string() })) .query(({ input }) => { diff --git a/apps/desktop/src/main/lib/browser/browser-manager.ts b/apps/desktop/src/main/lib/browser/browser-manager.ts index 3fe8d6201f6..d6c3c47f44a 100644 --- a/apps/desktop/src/main/lib/browser/browser-manager.ts +++ b/apps/desktop/src/main/lib/browser/browser-manager.ts @@ -49,6 +49,7 @@ class BrowserManager extends EventEmitter { wc.setWindowOpenHandler(({ url }) => { if (url && url !== "about:blank") { this.emit(`new-window:${paneId}`, url); + this.emit("new-window", { paneId, url }); } return { action: "deny" as const }; }); diff --git a/apps/desktop/src/renderer/hooks/useBrowserNewWindowHandler.ts b/apps/desktop/src/renderer/hooks/useBrowserNewWindowHandler.ts new file mode 100644 index 00000000000..797c68deb8e --- /dev/null +++ b/apps/desktop/src/renderer/hooks/useBrowserNewWindowHandler.ts @@ -0,0 +1,23 @@ +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useTabsStore } from "renderer/stores/tabs/store"; + +/** + * Global handler for new-window events from any browser pane. + * + * This must be mounted in a component that is **always rendered** (e.g. the + * dashboard layout) because webviews persist in a hidden container even when + * their BrowserPane component is unmounted. Without a persistent listener, + * target="_blank" clicks in hidden webviews would be silently lost. + */ +export function useBrowserNewWindowHandler() { + electronTrpc.browser.onAnyNewWindow.useSubscription(undefined, { + onData: ({ paneId, url }) => { + const state = useTabsStore.getState(); + const pane = state.panes[paneId]; + if (!pane) return; + const tab = state.tabs.find((t) => t.id === pane.tabId); + if (!tab) return; + state.addBrowserTab(tab.workspaceId, url); + }, + }); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index fe00ceed508..80401048ae0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -5,6 +5,7 @@ import { useNavigate, } from "@tanstack/react-router"; import { useFeatureFlagEnabled } from "posthog-js/react"; +import { useBrowserNewWindowHandler } from "renderer/hooks/useBrowserNewWindowHandler"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar"; import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel"; @@ -93,6 +94,11 @@ function DashboardLayout() { [openNewWorkspaceModal, currentWorkspace?.projectId], ); + // Global listener for target="_blank" / window.open in any browser pane. + // Must live here (always-mounted) because webviews persist in a hidden + // container even when their BrowserPane component is unmounted. + useBrowserNewWindowHandler(); + return (