fix: remove rel=noopener from all session/instance links to enable tab reuse

rel=noopener forces a fresh browsing context, breaking target name matching
and window reference tracking across browsers.

Changes:
- use-instance-actions.ts: track opened tabs in Map, call .focus() on existing
- session-card.tsx: all Open buttons now go through handleOpen callback
- app-shell.tsx: sidebar session links use target='session-{id}' + noreferrer
- workspace-instance-chips.tsx: remove noopener from chip links
- instance-list.tsx: remove noopener from Open links
- WorkspaceDetailPage.tsx: use named target + noreferrer

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
This commit is contained in:
Developer
2026-06-09 12:39:32 +00:00
parent e5e29aca49
commit 680417a0a2
6 changed files with 47 additions and 67 deletions
+2 -2
View File
@@ -50,8 +50,8 @@ const SessionItem = ({ session }: { session: Session }) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
target={`session-${session.id}`}
rel="noreferrer"
className="nav-item session-item"
title={`${session.display_name} (${session.status})`}
>
@@ -58,11 +58,6 @@ export function SessionCard({
const isTerminalOnly =
session.tool_type_interfaces?.includes("terminal") &&
!session.tool_type_interfaces?.includes("web");
const openHref = session.url
? session.url
: isTerminalOnly
? `/instances/${session.id}/terminal`
: undefined;
const hasTunnelError =
!isTerminalOnly && tunnelHealth?.tunnel_status === "unreachable";
const hasAppError =
@@ -138,13 +133,13 @@ export function SessionCard({
)}
{session.url && (
<p className="session-card-url">
<a
href={session.url}
target={`session-${session.id}`}
rel="noopener noreferrer"
<button
type="button"
className="link-button"
onClick={() => onOpen?.(session)}
>
{session.url}
</a>
</button>
</p>
)}
{session.created_at && (
@@ -159,27 +154,15 @@ export function SessionCard({
<div className="session-card-actions mobile">
{isActive && (
<>
{openHref ? (
<a
href={openHref}
target={`session-${session.id}`}
rel="noopener noreferrer"
className="secondary-button mobile-primary"
>
<Icon name="external" size="sm" />
Open
</a>
) : (
<button
className="secondary-button mobile-primary"
onClick={() => onOpen?.(session)}
type="button"
disabled={isBusy}
>
<Icon name="external" size="sm" />
Open
</button>
)}
<button
className="secondary-button mobile-primary"
onClick={() => onOpen?.(session)}
type="button"
disabled={isBusy}
>
<Icon name="external" size="sm" />
Open
</button>
<button
className="ghost-button mobile-more"
onClick={() => setShowActionSheet(true)}
@@ -216,27 +199,15 @@ export function SessionCard({
<div className="session-card-actions">
{isActive && (
<>
{openHref ? (
<a
href={openHref}
target={`session-${session.id}`}
rel="noopener noreferrer"
className="secondary-button small"
>
<Icon name="external" size="sm" />
<span className="action-label">Open</span>
</a>
) : (
<button
className="secondary-button small"
onClick={() => onOpen?.(session)}
type="button"
disabled={isBusy}
>
<Icon name="external" size="sm" />
<span className="action-label">Open</span>
</button>
)}
<button
className="secondary-button small"
onClick={() => onOpen?.(session)}
type="button"
disabled={isBusy}
>
<Icon name="external" size="sm" />
<span className="action-label">Open</span>
</button>
{!isTerminalOnly && onRecreateTunnel && (
<button
@@ -286,7 +286,7 @@ export const InstanceList = ({
: `${API_BASE_URL}${instance.url}`
}
target={`instance-${instance.id}`}
rel="noopener noreferrer"
rel="noreferrer"
className="secondary-button small"
>
<Icon name="external" size="sm" />
@@ -44,7 +44,7 @@ export function WorkspaceInstanceChips({
<a
href={inst.url}
target={`instance-${inst.id}`}
rel="noopener noreferrer"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
>
+18 -9
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import { useState, useCallback, useRef } from "react";
import {
stopInstance,
deleteInstance,
@@ -33,18 +33,27 @@ export function useInstanceActions(
null,
);
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
const tabRefs = useRef<Map<string, Window | null>>(new Map());
const handleOpen = useCallback((session: Session) => {
const tabName = `session-${session.id}`;
const key = session.id;
const existing = tabRefs.current.get(key);
if (existing && !existing.closed) {
existing.focus();
return;
}
let url: string;
if (session.url) {
window.open(session.url, tabName);
return;
url = session.url;
} else if (session.tool_type_interfaces?.includes("terminal")) {
url = `/instances/${session.id}/terminal`;
} else {
url = `/projects/${session.project_id}`;
}
if (session.tool_type_interfaces?.includes("terminal")) {
window.open(`/instances/${session.id}/terminal`, tabName);
return;
}
window.open(`/projects/${session.project_id}`, tabName);
const w = window.open(url, `session-${session.id}`);
tabRefs.current.set(key, w);
}, []);
const handleStart = useCallback(
+2 -2
View File
@@ -343,8 +343,8 @@ function ToolsTab({ workspace }: { workspace: Workspace }) {
{instance.url && (
<a
href={instance.url}
target="_blank"
rel="noopener noreferrer"
target={`instance-${instance.id}`}
rel="noreferrer"
>
Open
</a>