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 @@ -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)

Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/src/lib/trpc/routers/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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);
});
}),
Comment thread
MocA-Love marked this conversation as resolved.

getPageInfo: publicProcedure
.input(z.object({ paneId: z.string() }))
.query(({ input }) => {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/lib/browser/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/renderer/hooks/useBrowserNewWindowHandler.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div className="flex flex-col h-full w-full">
<TopBar />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { GlobeIcon } from "lucide-react";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { LuMinus, LuPlus } from "react-icons/lu";
import { TbDeviceDesktop } from "react-icons/tb";
import type { MosaicBranch } from "react-mosaic-component";
import { electronTrpc } from "renderer/lib/electron-trpc";
Expand Down Expand Up @@ -44,6 +45,8 @@ export function BrowserPane({
const isBlankPage = currentUrl === "about:blank";
const { mutate: openDevTools } =
electronTrpc.browser.openDevTools.useMutation();
const { mutate: setZoomLevel } =
electronTrpc.browser.setZoomLevel.useMutation();
Comment thread
MocA-Love marked this conversation as resolved.

const {
containerRef,
Expand All @@ -58,6 +61,34 @@ export function BrowserPane({
initialUrl: currentUrl,
});

// -- Zoom (synced with Electron's built-in Cmd+/- zoom) -----------------

const ZOOM_STEP = 1;
const ZOOM_MIN = -3;
const ZOOM_MAX = 5;

const [zoomLevel, setZoomLevelLocal] = useState(0);
const zoomPercent = Math.round(1.2 ** zoomLevel * 100);

// Sync when Cmd+/- changes zoom from keyboard
electronTrpc.browser.onZoomChanged.useSubscription(
{ paneId },
{
onData: ({ zoomLevel: level }) => {
setZoomLevelLocal(level);
},
},
);

const applyZoom = useCallback(
(level: number) => {
const clamped = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, level));
setZoomLevelLocal(clamped);
setZoomLevel({ paneId, level: clamped });
},
[paneId, setZoomLevel],
);

const handleOpenDevTools = useCallback(() => {
openDevTools({ paneId });
}, [openDevTools, paneId]);
Expand Down Expand Up @@ -92,6 +123,52 @@ export function BrowserPane({
closeHotkeyId="CLOSE_TERMINAL"
leadingActions={
<>
<div className="flex items-center gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => applyZoom(zoomLevel - ZOOM_STEP)}
disabled={zoomLevel <= ZOOM_MIN}
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground disabled:opacity-30"
>
<LuMinus className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
Zoom Out
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => applyZoom(0)}
className="rounded px-1 py-0.5 text-[10px] tabular-nums text-muted-foreground/60 transition-colors hover:text-muted-foreground"
>
{zoomPercent}%
</button>
Comment thread
MocA-Love marked this conversation as resolved.
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
Reset Zoom
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => applyZoom(zoomLevel + ZOOM_STEP)}
disabled={zoomLevel >= ZOOM_MAX}
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground disabled:opacity-30"
>
<LuPlus className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
Zoom In
</TooltipContent>
</Tooltip>
</div>
<Tooltip>
<TooltipTrigger asChild>
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { toast } from "@superset/ui/sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { useCallback, useEffect, useRef, useState } from "react";
import { LuLink } from "react-icons/lu";
import {
TbArrowLeft,
TbArrowRight,
Expand Down Expand Up @@ -184,28 +186,51 @@ export function BrowserToolbar({
/>
</form>
) : (
<button
type="button"
onClick={enterEditMode}
className="group flex w-full min-w-0 items-baseline rounded-sm border border-transparent px-2 py-0.5 text-left text-xs"
>
{isBlank ? (
<span className="text-muted-foreground/40">
Enter URL or search...
</span>
) : (
<>
<span className="min-w-0 truncate text-muted-foreground/60 transition-colors group-hover:text-foreground">
{url}
<div className="group flex w-full min-w-0 items-center">
<button
type="button"
onClick={enterEditMode}
className="flex flex-1 min-w-0 items-baseline rounded-sm border border-transparent px-2 py-0.5 text-left text-xs"
>
{isBlank ? (
<span className="text-muted-foreground/40">
Enter URL or search...
</span>
{pageTitle && (
<span className="min-w-0 ml-1 truncate text-muted-foreground/40 transition-opacity group-hover:opacity-0">
/ {pageTitle}
) : (
<>
<span className="min-w-0 truncate text-muted-foreground/60 transition-colors group-hover:text-foreground">
{url}
</span>
)}
</>
{pageTitle && (
<span className="min-w-0 ml-1 truncate text-muted-foreground/40 transition-opacity group-hover:opacity-0">
/ {pageTitle}
</span>
)}
</>
)}
</button>
{!isBlank && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(currentUrl).then(
() => toast.success("URL copied"),
() => toast.error("Failed to copy URL"),
);
}}
className="shrink-0 rounded p-0.5 text-muted-foreground/40 opacity-0 transition-all hover:text-muted-foreground group-hover:opacity-100"
>
<LuLink className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
Copy URL
</TooltipContent>
</Tooltip>
)}
</button>
</div>
)}
{isEditing && autocomplete.isOpen && (
<UrlSuggestions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,9 @@ export function usePersistentWebview({
const { mutate: upsertHistory } =
electronTrpc.browserHistory.upsert.useMutation();

// Subscribe to new-window events (target="_blank" links, window.open)
// handled via setWindowOpenHandler in the main process
electronTrpc.browser.onNewWindow.useSubscription(
{ paneId },
{
onData: ({ url }: { url: string }) => {
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.openInBrowserPane(tab.workspaceId, url);
},
},
);
// New-window events (target="_blank", window.open) are handled globally
// by useBrowserNewWindowHandler in the dashboard layout, so webviews that
// are parked in the hidden container still get their events handled.

// Subscribe to context menu actions (e.g. "Open Link as New Split")
electronTrpc.browser.onContextMenuAction.useSubscription(
Expand Down Expand Up @@ -260,6 +248,26 @@ export function usePersistentWebview({
}
`).catch(() => {});
}

// Cmd/Ctrl+click on links opens in a new browser tab.
// Chromium may not always trigger setWindowOpenHandler for modifier
// clicks, so we intercept them in the guest page and call window.open
// which is reliably caught by the handler.
wv.executeJavaScript(`
if (!window.__supersetCmdClickInstalled) {
window.__supersetCmdClickInstalled = true;
document.addEventListener('click', function(e) {
if (!(e.metaKey || e.ctrlKey) || e.button !== 0) return;
var el = e.target;
while (el && el.tagName !== 'A') el = el.parentElement;
if (el && el.href && !el.href.startsWith('javascript:')) {
e.preventDefault();
e.stopPropagation();
window.open(el.href, '_blank');
}
}, true);
}
`).catch(() => {});
};

const handleDidStartLoading = () => {
Expand Down