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