fix: remove features string from window.open to enable tab reuse

window.open(url, name, 'noopener,noreferrer') with a non-empty features
string forces a new popup window and ignores the name for tab reuse.
Remove the third parameter so the browser focuses existing named tabs.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
This commit is contained in:
Developer
2026-06-09 12:18:28 +00:00
parent 486f3cbc44
commit e5e29aca49
3 changed files with 223 additions and 222 deletions
@@ -138,7 +138,11 @@ export function SessionCard({
)} )}
{session.url && ( {session.url && (
<p className="session-card-url"> <p className="session-card-url">
<a href={session.url} target={`session-${session.id}`} rel="noopener noreferrer"> <a
href={session.url}
target={`session-${session.id}`}
rel="noopener noreferrer"
>
{session.url} {session.url}
</a> </a>
</p> </p>
@@ -11,7 +11,10 @@ import {
} from "../../../api/sessions"; } from "../../../api/sessions";
import type { ToolType } from "../../../api/tool-types"; import type { ToolType } from "../../../api/tool-types";
import { CreateSessionForm } from "../session/create-session-form"; import { CreateSessionForm } from "../session/create-session-form";
import { listConfigProfiles, type ConfigProfile } from "../../../api/config-profiles"; import {
listConfigProfiles,
type ConfigProfile,
} from "../../../api/config-profiles";
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys"; import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
import { useEventContext } from "../../../state/events"; import { useEventContext } from "../../../state/events";
@@ -307,120 +310,117 @@ export const InstanceList = ({
)} )}
{instance.status !== "running" && ( {instance.status !== "running" && (
<> <>
{profileSelectInstanceId === instance.id ? ( {profileSelectInstanceId === instance.id ? (
<div className="inline-profile-select"> <div className="inline-profile-select">
<select <select
value={selectedProfileForAction} value={selectedProfileForAction}
onChange={(e) => onChange={(e) =>
setSelectedProfileForAction(e.target.value) setSelectedProfileForAction(e.target.value)
} }
> >
<option value="">Default (none)</option> <option value="">Default (none)</option>
{configProfiles.map((p) => ( {configProfiles.map((p) => (
<option key={p.id} value={p.id}> <option key={p.id} value={p.id}>
{p.name} {p.name}
</option> </option>
))} ))}
</select> </select>
<div <div
style={{
display: "flex",
flexWrap: "wrap",
gap: "0.25rem",
marginTop: "0.25rem",
}}
>
{sshKeys.map((key) => (
<label
key={key.id}
className="checkbox-label"
style={{ style={{
fontSize: "0.75rem",
display: "flex", display: "flex",
alignItems: "center", flexWrap: "wrap",
gap: "0.25rem", gap: "0.25rem",
marginTop: "0.25rem",
}} }}
> >
<input {sshKeys.map((key) => (
type="checkbox" <label
checked={selectedSshKeyIdsForAction.includes( key={key.id}
key.id, className="checkbox-label"
)} style={{
onChange={(e) => { fontSize: "0.75rem",
if (e.target.checked) { display: "flex",
setSelectedSshKeyIdsForAction( alignItems: "center",
(prev) => [...prev, key.id], gap: "0.25rem",
); }}
} else { >
setSelectedSshKeyIdsForAction( <input
(prev) => type="checkbox"
prev.filter( checked={selectedSshKeyIdsForAction.includes(
(id) => key.id,
id !== key.id, )}
), onChange={(e) => {
); if (e.target.checked) {
} setSelectedSshKeyIdsForAction((prev) => [
}} ...prev,
/> key.id,
{key.name} ]);
</label> } else {
))} setSelectedSshKeyIdsForAction((prev) =>
</div> prev.filter((id) => id !== key.id),
<button );
className="primary-button small" }
onClick={() => }}
void handleStart( />
instance.id, {key.name}
selectedProfileForAction || undefined, </label>
selectedSshKeyIdsForAction.length > 0 ))}
? selectedSshKeyIdsForAction </div>
: undefined, <button
) className="primary-button small"
} onClick={() =>
type="button" void handleStart(
disabled={busyInstanceId === instance.id} instance.id,
> selectedProfileForAction || undefined,
<Icon name="play" size="sm" /> selectedSshKeyIdsForAction.length > 0
Start ? selectedSshKeyIdsForAction
</button> : undefined,
<button )
className="ghost-button small" }
onClick={() => { type="button"
setProfileSelectInstanceId(null); disabled={busyInstanceId === instance.id}
setSelectedProfileForAction(""); >
setSelectedSshKeyIdsForAction([]); <Icon name="play" size="sm" />
}} Start
type="button" </button>
disabled={busyInstanceId === instance.id} <button
> className="ghost-button small"
Cancel onClick={() => {
</button> setProfileSelectInstanceId(null);
</div> setSelectedProfileForAction("");
) : ( setSelectedSshKeyIdsForAction([]);
<button }}
className="secondary-button small" type="button"
onClick={() => { disabled={busyInstanceId === instance.id}
const toolType = toolTypes.find( >
(t) => t.id === instance.tool_type_id, Cancel
); </button>
if (toolType) { </div>
void loadConfigProfiles(toolType.id); ) : (
} <button
setProfileSelectInstanceId(instance.id); className="secondary-button small"
setSelectedProfileForAction( onClick={() => {
instance.selected_config_profile_id || "", const toolType = toolTypes.find(
); (t) => t.id === instance.tool_type_id,
setSelectedSshKeyIdsForAction( );
instance.ssh_key_ids || [], if (toolType) {
); void loadConfigProfiles(toolType.id);
}} }
type="button" setProfileSelectInstanceId(instance.id);
disabled={busyInstanceId === instance.id} setSelectedProfileForAction(
> instance.selected_config_profile_id || "",
<Icon name="play" size="sm" /> );
Start setSelectedSshKeyIdsForAction(
</button> instance.ssh_key_ids || [],
)} );
}}
type="button"
disabled={busyInstanceId === instance.id}
>
<Icon name="play" size="sm" />
Start
</button>
)}
</> </>
)} )}
{instance.status === "running" && ( {instance.status === "running" && (
@@ -455,119 +455,116 @@ export const InstanceList = ({
<Icon name="stop" size="sm" /> <Icon name="stop" size="sm" />
</button> </button>
)} )}
{profileSelectInstanceId === instance.id ? ( {profileSelectInstanceId === instance.id ? (
<div className="inline-profile-select"> <div className="inline-profile-select">
<select <select
value={selectedProfileForAction} value={selectedProfileForAction}
onChange={(e) => onChange={(e) =>
setSelectedProfileForAction(e.target.value) setSelectedProfileForAction(e.target.value)
} }
> >
<option value="">Default (none)</option> <option value="">Default (none)</option>
{configProfiles.map((p) => ( {configProfiles.map((p) => (
<option key={p.id} value={p.id}> <option key={p.id} value={p.id}>
{p.name} {p.name}
</option> </option>
))} ))}
</select> </select>
<div <div
style={{
display: "flex",
flexWrap: "wrap",
gap: "0.25rem",
marginTop: "0.25rem",
}}
>
{sshKeys.map((key) => (
<label
key={key.id}
className="checkbox-label"
style={{ style={{
fontSize: "0.75rem",
display: "flex", display: "flex",
alignItems: "center", flexWrap: "wrap",
gap: "0.25rem", gap: "0.25rem",
marginTop: "0.25rem",
}} }}
> >
<input {sshKeys.map((key) => (
type="checkbox" <label
checked={selectedSshKeyIdsForAction.includes( key={key.id}
key.id, className="checkbox-label"
)} style={{
onChange={(e) => { fontSize: "0.75rem",
if (e.target.checked) { display: "flex",
setSelectedSshKeyIdsForAction( alignItems: "center",
(prev) => [...prev, key.id], gap: "0.25rem",
); }}
} else { >
setSelectedSshKeyIdsForAction( <input
(prev) => type="checkbox"
prev.filter( checked={selectedSshKeyIdsForAction.includes(
(id) => key.id,
id !== key.id, )}
), onChange={(e) => {
); if (e.target.checked) {
} setSelectedSshKeyIdsForAction((prev) => [
}} ...prev,
/> key.id,
{key.name} ]);
</label> } else {
))} setSelectedSshKeyIdsForAction((prev) =>
</div> prev.filter((id) => id !== key.id),
<button );
className="primary-button small" }
onClick={() => }}
void handleRestart( />
instance.id, {key.name}
selectedProfileForAction || undefined, </label>
selectedSshKeyIdsForAction.length > 0 ))}
? selectedSshKeyIdsForAction </div>
: undefined, <button
) className="primary-button small"
} onClick={() =>
type="button" void handleRestart(
disabled={busyInstanceId === instance.id} instance.id,
> selectedProfileForAction || undefined,
<Icon name="refresh" size="sm" /> selectedSshKeyIdsForAction.length > 0
Restart ? selectedSshKeyIdsForAction
</button> : undefined,
<button )
className="ghost-button small" }
onClick={() => { type="button"
setProfileSelectInstanceId(null); disabled={busyInstanceId === instance.id}
setSelectedProfileForAction(""); >
setSelectedSshKeyIdsForAction([]); <Icon name="refresh" size="sm" />
}} Restart
type="button" </button>
disabled={busyInstanceId === instance.id} <button
> className="ghost-button small"
Cancel onClick={() => {
</button> setProfileSelectInstanceId(null);
</div> setSelectedProfileForAction("");
) : ( setSelectedSshKeyIdsForAction([]);
<button }}
className="ghost-button small" type="button"
onClick={() => { disabled={busyInstanceId === instance.id}
const toolType = toolTypes.find( >
(t) => t.id === instance.tool_type_id, Cancel
); </button>
if (toolType) { </div>
void loadConfigProfiles(toolType.id); ) : (
} <button
setProfileSelectInstanceId(instance.id); className="ghost-button small"
setSelectedProfileForAction( onClick={() => {
instance.selected_config_profile_id || "", const toolType = toolTypes.find(
); (t) => t.id === instance.tool_type_id,
setSelectedSshKeyIdsForAction( );
instance.ssh_key_ids || [], if (toolType) {
); void loadConfigProfiles(toolType.id);
}} }
type="button" setProfileSelectInstanceId(instance.id);
disabled={busyInstanceId === instance.id} setSelectedProfileForAction(
> instance.selected_config_profile_id || "",
<Icon name="refresh" size="sm" /> );
</button> setSelectedSshKeyIdsForAction(
)} instance.ssh_key_ids || [],
);
}}
type="button"
disabled={busyInstanceId === instance.id}
>
<Icon name="refresh" size="sm" />
</button>
)}
</> </>
)} )}
<button <button
+3 -3
View File
@@ -37,14 +37,14 @@ export function useInstanceActions(
const handleOpen = useCallback((session: Session) => { const handleOpen = useCallback((session: Session) => {
const tabName = `session-${session.id}`; const tabName = `session-${session.id}`;
if (session.url) { if (session.url) {
window.open(session.url, tabName, "noopener,noreferrer"); window.open(session.url, tabName);
return; return;
} }
if (session.tool_type_interfaces?.includes("terminal")) { if (session.tool_type_interfaces?.includes("terminal")) {
window.open(`/instances/${session.id}/terminal`, tabName, "noopener,noreferrer"); window.open(`/instances/${session.id}/terminal`, tabName);
return; return;
} }
window.open(`/projects/${session.project_id}`, tabName, "noopener,noreferrer"); window.open(`/projects/${session.project_id}`, tabName);
}, []); }, []);
const handleStart = useCallback( const handleStart = useCallback(