mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
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:
1 parent
56f8c59f1b
commit
0c39f19272
3 files changed
+87
-11
No files matched your search
Generated
+57
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user