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:
@@ -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()}
|
||||
>
|
||||
↗
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user