diff --git a/README.md b/README.md index af13096f488..81f29c407fe 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Works with any CLI agent. Built for local worktree-based development. | **AI コミットメッセージ生成** | コミットメッセージ入力欄のスパークルボタンで AI が conventional commit メッセージを日本語で自動生成。階層的要約方式(gptcommit 式)により大量差分でも高精度。staged/unstaged/untracked 全対応、lock ファイル・バイナリ自動スキップ | [#4](https://github.com/MocA-Love/superset/pull/4) | 2026-03-28 | | **ポートリストのリサイズ・フィルタ** | サイドバーの Ports セクションの高さをドラッグでリサイズ可能に(80–600px、永続化)。フィルタトグルで ports.json に定義されたポートのみ表示し、自動検出ポートを非表示にできる | [#6](https://github.com/MocA-Love/superset/pull/6) | 2026-03-28 | | **大規模ファイル 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 | ## Fork のビルド方法 (macOS) diff --git a/apps/desktop/src/lib/trpc/routers/ports/ports.ts b/apps/desktop/src/lib/trpc/routers/ports/ports.ts index d4432be08a9..089a91c9032 100644 --- a/apps/desktop/src/lib/trpc/routers/ports/ports.ts +++ b/apps/desktop/src/lib/trpc/routers/ports/ports.ts @@ -1,6 +1,5 @@ import { workspaces } from "@superset/local-db"; import { observable } from "@trpc/server/observable"; -import { eq } from "drizzle-orm"; import { localDb } from "main/lib/local-db"; import { loadStaticPorts } from "main/lib/static-ports"; import { portManager } from "main/lib/terminal/port-manager"; @@ -24,30 +23,80 @@ function getLabelsForPath(worktreePath: string): Map | null { return labels; } +/** Cache structure for workspace path + labels lookup. */ +interface WorkspaceLabelInfo { + labels: Map | null; + workspaceId: string; +} + +function buildLabelCache(): Map { + const cache = new Map(); + const allWs = localDb.select().from(workspaces).all(); + + for (const ws of allWs) { + const wsPath = getWorkspacePath(ws); + if (!wsPath) continue; + const labels = getLabelsForPath(wsPath); + if (labels) { + cache.set(ws.id, { labels, workspaceId: ws.id }); + } + } + + return cache; +} + export const createPortsRouter = () => { return router({ getAll: publicProcedure.query((): EnrichedPort[] => { const detectedPorts = portManager.getAllPorts(); + const labelCache = buildLabelCache(); - const labelCache = new Map | null>(); - - return detectedPorts.map((port) => { - if (!labelCache.has(port.workspaceId)) { - const ws = localDb - .select() - .from(workspaces) - .where(eq(workspaces.id, port.workspaceId)) - .get(); - const wsPath = ws ? getWorkspacePath(ws) : null; - labelCache.set( - port.workspaceId, - wsPath ? getLabelsForPath(wsPath) : null, - ); - } + // Track which static ports have been matched with detected ports + // key: "workspaceId:port" + const matchedStaticPorts = new Set(); - const labels = labelCache.get(port.workspaceId); - return { ...port, label: labels?.get(port.port) ?? null }; + // Enrich detected ports with labels + const enriched: EnrichedPort[] = detectedPorts.map((port) => { + const info = labelCache.get(port.workspaceId); + const label = info?.labels.get(port.port) ?? null; + if (label != null) { + matchedStaticPorts.add(`${port.workspaceId}:${port.port}`); + } + return { + port: port.port, + workspaceId: port.workspaceId, + label, + detected: true, + pid: port.pid, + processName: port.processName, + paneId: port.paneId, + detectedAt: port.detectedAt, + address: port.address, + }; }); + + // Add static ports that were NOT detected + for (const [wsId, info] of labelCache) { + if (!info.labels) continue; + for (const [portNum, label] of info.labels) { + const key = `${wsId}:${portNum}`; + if (matchedStaticPorts.has(key)) continue; + + enriched.push({ + port: portNum, + workspaceId: wsId, + label, + detected: false, + pid: null, + processName: null, + paneId: null, + detectedAt: null, + address: null, + }); + } + } + + return enriched; }), subscribe: publicProcedure.subscription(() => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx index 600023673bd..0c787b53ab8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx @@ -22,10 +22,14 @@ export function MergedPortBadge({ port }: MergedPortBadgeProps) { const openUrl = electronTrpc.external.openUrl.useMutation(); const { killPort } = useKillPort(); + const isDetected = port.detected; + const displayContent = port.label ? ( <> {port.label}{" "} - + {port.port} @@ -65,7 +69,13 @@ export function MergedPortBadge({ port }: MergedPortBadgeProps) { return ( -
+
- - + {isDetected && ( + <> + + + + )}
@@ -100,12 +117,17 @@ export function MergedPortBadge({ port }: MergedPortBadgeProps) { > localhost:{port.port}
- {(port.processName || port.pid != null) && ( + {isDetected && (port.processName || port.pid != null) && (
{port.processName} {port.pid != null && ` (pid ${port.pid})`}
)} + {!isDetected && ( +
+ Not detected +
+ )} {canJumpToTerminal && (
Click to open workspace diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/WorkspacePortGroup.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/WorkspacePortGroup.tsx index 4c9e5e3fdb9..198cc5720ea 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/WorkspacePortGroup.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/WorkspacePortGroup.tsx @@ -19,8 +19,10 @@ export function WorkspacePortGroup({ group }: WorkspacePortGroupProps) { navigateToWorkspace(group.workspaceId, navigate); }; + const detectedPorts = group.ports.filter((p) => p.detected); + const handleCloseAll = () => { - killPorts(group.ports); + killPorts(detectedPorts); }; return ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/useKillPort.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/useKillPort.ts index 8d3b75cd259..ac3bc3fda91 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/useKillPort.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/useKillPort.ts @@ -6,6 +6,7 @@ export function useKillPort() { const killMutation = electronTrpc.ports.kill.useMutation(); const killPort = async (port: EnrichedPort) => { + if (!port.paneId) return; const result = await killMutation.mutateAsync({ paneId: port.paneId, port: port.port, @@ -18,12 +19,13 @@ export function useKillPort() { }; const killPorts = async (ports: EnrichedPort[]) => { - if (ports.length === 0) return; + const killable = ports.filter((p) => p.paneId != null); + if (killable.length === 0) return; const results = await Promise.all( - ports.map((port) => + killable.map((port) => killMutation.mutateAsync({ - paneId: port.paneId, + paneId: port.paneId as string, port: port.port, }), ), diff --git a/apps/desktop/src/shared/types/ports.ts b/apps/desktop/src/shared/types/ports.ts index 967a9a2a20d..c676bb7ed6c 100644 --- a/apps/desktop/src/shared/types/ports.ts +++ b/apps/desktop/src/shared/types/ports.ts @@ -20,6 +20,16 @@ export interface StaticPortsResult { error: string | null; } -export interface EnrichedPort extends DetectedPort { +export interface EnrichedPort { + port: number; + workspaceId: string; label: string | null; + /** Whether this port is currently detected as listening. */ + detected: boolean; + /** Detection info — only present when `detected` is true. */ + pid: number | null; + processName: string | null; + paneId: string | null; + detectedAt: number | null; + address: string | null; }