Fix custom image refresh terminal flow

Auto-open the browser terminal for SSH custom image setup sessions and keep Refresh on the returned active session so stale overview refetches cannot fall back to the old active-template panel. Demote the raw SSH command to an explicit fallback and cover the Refresh path in the UI test. Update the lockfile for the existing ssh2 server dependency so frozen installs pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Cody committed 2026-07-02 15:44:39 +00:00
1 parent 56f8c59f1b
commit 0c39f19272
3 files changed
+87 -11

No files matched your search

+57
View File
@@ -785,6 +785,9 @@ importers:
sharp:
specifier: ^0.35.2
version: 0.35.2
ssh2:
specifier: ^1.17.0
version: 1.17.0
ws:
specifier: ^8.19.0
version: 8.19.0
@@ -4990,6 +4993,9 @@ packages:
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -5098,6 +5104,9 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
better-auth@1.6.20:
resolution: {integrity: sha512-fSpGHGRKiGRiYVd3QTQtuVZ8oxpiSe/7ip0Rpvt/Sy8zQbEbVKUPMOhE0gLXg+FjqTUsIo7582hxUYxtEcqUpA==}
peerDependencies:
@@ -5210,6 +5219,10 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
buildcheck@0.0.7:
resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
engines: {node: '>=10.0.0'}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@@ -5394,6 +5407,10 @@ packages:
cose-base@2.2.0:
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
cpu-features@0.0.10:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
engines: {node: '>=10.0.0'}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@@ -6833,6 +6850,9 @@ packages:
resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==}
engines: {node: '>= 10.16.0'}
nan@2.28.0:
resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -7559,6 +7579,10 @@ packages:
sqlite3@5.1.7:
resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==}
ssh2@1.17.0:
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
engines: {node: '>=10.16.0'}
ssri@8.0.1:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'}
@@ -7780,6 +7804,9 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -12315,6 +12342,10 @@ snapshots:
asap@2.0.6: {}
asn1@0.2.6:
dependencies:
safer-buffer: 2.1.2
assertion-error@2.0.1: {}
assistant-cloud@0.1.31:
@@ -12390,6 +12421,10 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
bcrypt-pbkdf@1.0.2:
dependencies:
tweetnacl: 0.14.5
better-auth@1.6.20(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.15)(kysely@0.29.2)(pg@8.18.0)(postgres@3.4.9)(sqlite3@5.1.7))(pg@8.18.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.8(@types/node@22.19.21)(jsdom@28.1.0(@noble/hashes@2.2.0))(vite@6.4.1(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))):
dependencies:
'@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@3.25.76))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
@@ -12500,6 +12535,9 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
buildcheck@0.0.7:
optional: true
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
@@ -12685,6 +12723,12 @@ snapshots:
dependencies:
layout-base: 2.0.1
cpu-features@0.0.10:
dependencies:
buildcheck: 0.0.7
nan: 2.28.0
optional: true
crelt@1.0.6: {}
cross-env@10.1.0:
@@ -14499,6 +14543,9 @@ snapshots:
concat-stream: 2.0.0
type-is: 1.6.18
nan@2.28.0:
optional: true
nanoid@3.3.11: {}
nanoid@3.3.15: {}
@@ -15494,6 +15541,14 @@ snapshots:
- supports-color
optional: true
ssh2@1.17.0:
dependencies:
asn1: 0.2.6
bcrypt-pbkdf: 1.0.2
optionalDependencies:
cpu-features: 0.0.10
nan: 2.28.0
ssri@8.0.1:
dependencies:
minipass: 3.3.6
@@ -15753,6 +15808,8 @@ snapshots:
safe-buffer: 5.2.1
optional: true
tweetnacl@0.14.5: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
+9 -6
View File
@@ -629,7 +629,7 @@ describe("CompanyEnvironments — test provider button", () => {
expect(getOpenDialog()?.textContent).not.toContain(command);
});
it("opens an embedded browser terminal while preserving the SSH command fallback", async () => {
it("opens an embedded browser terminal automatically while preserving the SSH command fallback", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const command = "ssh sandbox@setup.example.invalid -p 2222";
@@ -661,11 +661,8 @@ describe("CompanyEnvironments — test provider button", () => {
await act(async () => click(editButtons(container)[0]));
await waitForAssertion(() => {
expect(getOpenDialog()?.textContent).toContain(command);
expect(findButton(getOpenDialog()!, "Open terminal")).toBeTruthy();
});
await act(async () => click(findButton(getOpenDialog()!, "Open terminal")));
await waitForAssertion(() => {
expect(getOpenDialog()?.textContent).toContain("Browser terminal");
expect(getOpenDialog()?.textContent).toContain("SSH command fallback");
expect(mockEnvironmentsApi.createCustomImageTerminalSessionToken).toHaveBeenCalledExactlyOnceWith("session-1", {});
expect(FakeWebSocket.instances).toHaveLength(1);
});
@@ -885,5 +882,11 @@ describe("CompanyEnvironments — test provider button", () => {
"company-1",
{ templateId: "template-active" },
);
await waitForAssertion(() => {
expect(getOpenDialog()?.textContent).toContain("Browser terminal");
expect(getOpenDialog()?.textContent).toContain("SSH command fallback");
expect(mockEnvironmentsApi.createCustomImageTerminalSessionToken).toHaveBeenCalledExactlyOnceWith("session-1", {});
expect(FakeWebSocket.instances).toHaveLength(1);
});
});
});
+21 -5
View File
@@ -313,8 +313,10 @@ function customImageTerminalStatusCopy(state: CustomImageTerminalConnectionState
}
function EnvironmentCustomImageBrowserTerminal({
autoConnect = false,
sessionId,
}: {
autoConnect?: boolean;
sessionId: string;
}) {
const [connectionState, setConnectionState] = useState<CustomImageTerminalConnectionState>("idle");
@@ -322,6 +324,7 @@ function EnvironmentCustomImageBrowserTerminal({
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const terminalRef = useRef<HTMLDivElement | null>(null);
const socketRef = useRef<WebSocket | null>(null);
const autoConnectAttemptedSessionRef = useRef<string | null>(null);
const closeSocket = useCallback((reason = "operator_closed") => {
const socket = socketRef.current;
@@ -335,6 +338,7 @@ function EnvironmentCustomImageBrowserTerminal({
useEffect(() => {
closeSocket("session_changed");
autoConnectAttemptedSessionRef.current = null;
setConnectionState("idle");
setOutput("");
setErrorMessage(null);
@@ -412,6 +416,16 @@ function EnvironmentCustomImageBrowserTerminal({
}
}, [closeSocket, sessionId]);
useEffect(() => {
if (!autoConnect || connectionState !== "idle") return;
if (autoConnectAttemptedSessionRef.current === sessionId) return;
const timeoutId = window.setTimeout(() => {
autoConnectAttemptedSessionRef.current = sessionId;
void connectTerminal();
}, 0);
return () => window.clearTimeout(timeoutId);
}, [autoConnect, connectTerminal, connectionState, sessionId]);
const sendTerminalInput = useCallback((data: string) => {
const socket = socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN) return;
@@ -581,7 +595,6 @@ function EnvironmentImageTemplatePanel({
latestSession: result.session,
}));
setSessionResult(result);
invalidateOverview();
pushToast({
title: "Setup session started",
body: "Connect details are available while the session is active.",
@@ -782,14 +795,17 @@ function EnvironmentImageTemplatePanel({
</div>
</div>
{session.status === "waiting_for_user" && connectionPayload?.type === "ssh" ? (
<EnvironmentCustomImageBrowserTerminal sessionId={session.id} />
<EnvironmentCustomImageBrowserTerminal autoConnect sessionId={session.id} />
) : null}
{session.status === "waiting_for_user" && connectionCommand ? (
<div className="mt-3 flex flex-wrap items-center gap-2 rounded-md bg-muted/30 px-3 py-2">
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap text-[11px] leading-5">
<details className="mt-2 rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
<summary className="cursor-pointer select-none font-medium text-foreground">
SSH command fallback
</summary>
<code className="mt-2 block overflow-x-auto whitespace-nowrap text-[11px] leading-5">
{connectionCommand}
</code>
</div>
</details>
) : null}
{session.status === "waiting_for_user" && connectionFallbackMessage ? (
<div className="mt-2 text-xs text-muted-foreground">