| author | |
| committer | |
| log | 5b3b31b6e6846eb0509fb2e7c31646ae843c5609 |
| tree | 289c883b2ee04e7c7902db4ae37835048b595266 |
| parent | b7559c467803f923e59a287c112b4c92b96e15c7 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 231 insertions(+), 45 deletions(-)
apps/web/src/authBootstrap.test.ts+41| ... | ... | @@ -175,6 +175,47 @@ describe("resolveInitialServerAuthGateState", () => { |
| 175 | 175 | }); |
| 176 | 176 | }); |
| 177 | 177 | |
| 178 | it("uses the vite proxy for desktop-managed loopback auth requests during local dev", async () => { | |
| 179 | const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce( | |
| 180 | sessionResponse({ | |
| 181 | authenticated: false, | |
| 182 | auth: { | |
| 183 | policy: "desktop-managed-local", | |
| 184 | bootstrapMethods: ["desktop-bootstrap"], | |
| 185 | sessionMethods: ["browser-session-cookie"], | |
| 186 | sessionCookieName: "t3_session", | |
| 187 | }, | |
| 188 | }), | |
| 189 | ); | |
| 190 | vi.stubGlobal("fetch", fetchMock); | |
| 191 | vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733"); | |
| 192 | ||
| 193 | const testWindow = installTestBrowser("http://127.0.0.1:5733/"); | |
| 194 | testWindow.desktopBridge = { | |
| 195 | getLocalEnvironmentBootstrap: () => ({ | |
| 196 | label: "Local environment", | |
| 197 | httpBaseUrl: "http://127.0.0.1:3773", | |
| 198 | wsBaseUrl: "ws://127.0.0.1:3773", | |
| 199 | }), | |
| 200 | } as DesktopBridge; | |
| 201 | ||
| 202 | const { resolveInitialServerAuthGateState } = await import("./environments/primary"); | |
| 203 | ||
| 204 | await expect(resolveInitialServerAuthGateState()).resolves.toEqual({ | |
| 205 | status: "requires-auth", | |
| 206 | auth: { | |
| 207 | policy: "desktop-managed-local", | |
| 208 | bootstrapMethods: ["desktop-bootstrap"], | |
| 209 | sessionMethods: ["browser-session-cookie"], | |
| 210 | sessionCookieName: "t3_session", | |
| 211 | }, | |
| 212 | }); | |
| 213 | ||
| 214 | expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:5733/api/auth/session", { | |
| 215 | credentials: "include", | |
| 216 | }); | |
| 217 | }); | |
| 218 | ||
| 178 | 219 | it("returns a requires-auth state instead of throwing when no bootstrap credential exists", async () => { |
| 179 | 220 | const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce( |
| 180 | 221 | sessionResponse({ |
apps/web/src/components/ChatView.tsx+27-25| ... | ... | @@ -31,7 +31,7 @@ import { |
| 31 | 31 | import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model"; |
| 32 | 32 | import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; |
| 33 | 33 | import { truncate } from "@t3tools/shared/String"; |
| 34 | import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | |
| 34 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | |
| 35 | 35 | import { useQuery } from "@tanstack/react-query"; |
| 36 | 36 | import { useDebouncedValue } from "@tanstack/react-pacer"; |
| 37 | 37 | import { useNavigate, useSearch } from "@tanstack/react-router"; |
| ... | ... | @@ -491,7 +491,7 @@ interface PersistentThreadTerminalDrawerProps { |
| 491 | 491 | onAddTerminalContext: (selection: TerminalContextSelection) => void; |
| 492 | 492 | } |
| 493 | 493 | |
| 494 | function PersistentThreadTerminalDrawer({ | |
| 494 | const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDrawer({ | |
| 495 | 495 | threadRef, |
| 496 | 496 | threadId, |
| 497 | 497 | visible, |
| ... | ... | @@ -650,7 +650,7 @@ function PersistentThreadTerminalDrawer({ |
| 650 | 650 | /> |
| 651 | 651 | </div> |
| 652 | 652 | ); |
| 653 | } | |
| 653 | }); | |
| 654 | 654 | |
| 655 | 655 | export default function ChatView(props: ChatViewProps) { |
| 656 | 656 | const { environmentId, threadId, routeKind } = props; |
| ... | ... | @@ -867,6 +867,14 @@ export default function ChatView(props: ChatViewProps) { |
| 867 | 867 | [draftThreadsByThreadKey], |
| 868 | 868 | ); |
| 869 | 869 | const [mountedTerminalThreadKeys, setMountedTerminalThreadKeys] = useState<string[]>([]); |
| 870 | const mountedTerminalThreadRefs = useMemo( | |
| 871 | () => | |
| 872 | mountedTerminalThreadKeys.flatMap((mountedThreadKey) => { | |
| 873 | const mountedThreadRef = parseScopedThreadKey(mountedThreadKey); | |
| 874 | return mountedThreadRef ? [{ key: mountedThreadKey, threadRef: mountedThreadRef }] : []; | |
| 875 | }), | |
| 876 | [mountedTerminalThreadKeys], | |
| 877 | ); | |
| 870 | 878 | |
| 871 | 879 | const setPrompt = useCallback( |
| 872 | 880 | (nextPrompt: string) => { |
| ... | ... | @@ -4850,28 +4858,22 @@ export default function ChatView(props: ChatViewProps) { |
| 4850 | 4858 | </div> |
| 4851 | 4859 | {/* end horizontal flex container */} |
| 4852 | 4860 | |
| 4853 | {mountedTerminalThreadKeys.flatMap((mountedThreadKey) => { | |
| 4854 | const mountedThreadRef = parseScopedThreadKey(mountedThreadKey); | |
| 4855 | if (!mountedThreadRef) { | |
| 4856 | return []; | |
| 4857 | } | |
| 4858 | return [ | |
| 4859 | <PersistentThreadTerminalDrawer | |
| 4860 | key={mountedThreadKey} | |
| 4861 | threadRef={mountedThreadRef} | |
| 4862 | threadId={mountedThreadRef.threadId} | |
| 4863 | visible={mountedThreadKey === activeThreadKey && terminalState.terminalOpen} | |
| 4864 | launchContext={ | |
| 4865 | mountedThreadKey === activeThreadKey ? (activeTerminalLaunchContext ?? null) : null | |
| 4866 | } | |
| 4867 | focusRequestId={mountedThreadKey === activeThreadKey ? terminalFocusRequestId : 0} | |
| 4868 | splitShortcutLabel={splitTerminalShortcutLabel ?? undefined} | |
| 4869 | newShortcutLabel={newTerminalShortcutLabel ?? undefined} | |
| 4870 | closeShortcutLabel={closeTerminalShortcutLabel ?? undefined} | |
| 4871 | onAddTerminalContext={addTerminalContextToDraft} | |
| 4872 | />, | |
| 4873 | ]; | |
| 4874 | })} | |
| 4861 | {mountedTerminalThreadRefs.map(({ key: mountedThreadKey, threadRef: mountedThreadRef }) => ( | |
| 4862 | <PersistentThreadTerminalDrawer | |
| 4863 | key={mountedThreadKey} | |
| 4864 | threadRef={mountedThreadRef} | |
| 4865 | threadId={mountedThreadRef.threadId} | |
| 4866 | visible={mountedThreadKey === activeThreadKey && terminalState.terminalOpen} | |
| 4867 | launchContext={ | |
| 4868 | mountedThreadKey === activeThreadKey ? (activeTerminalLaunchContext ?? null) : null | |
| 4869 | } | |
| 4870 | focusRequestId={mountedThreadKey === activeThreadKey ? terminalFocusRequestId : 0} | |
| 4871 | splitShortcutLabel={splitTerminalShortcutLabel ?? undefined} | |
| 4872 | newShortcutLabel={newTerminalShortcutLabel ?? undefined} | |
| 4873 | closeShortcutLabel={closeTerminalShortcutLabel ?? undefined} | |
| 4874 | onAddTerminalContext={addTerminalContextToDraft} | |
| 4875 | /> | |
| 4876 | ))} | |
| 4875 | 4877 | |
| 4876 | 4878 | {expandedImage && expandedImageItem && ( |
| 4877 | 4879 | <div |
apps/web/src/components/ThreadTerminalDrawer.browser.tsx+26| ... | ... | @@ -259,6 +259,32 @@ describe("TerminalViewport", () => { |
| 259 | 259 | } |
| 260 | 260 | }); |
| 261 | 261 | |
| 262 | it("does not reopen the terminal when the scoped thread reference values stay the same", async () => { | |
| 263 | const environment = createEnvironmentApi(); | |
| 264 | environmentApiById.set("environment-a", environment); | |
| 265 | ||
| 266 | const mounted = await mountTerminalViewport({ | |
| 267 | threadRef: scopeThreadRef("environment-a" as never, THREAD_ID), | |
| 268 | }); | |
| 269 | ||
| 270 | try { | |
| 271 | await vi.waitFor(() => { | |
| 272 | expect(environment.terminal.open).toHaveBeenCalledTimes(1); | |
| 273 | }); | |
| 274 | ||
| 275 | await mounted.rerender({ | |
| 276 | threadRef: scopeThreadRef("environment-a" as never, THREAD_ID), | |
| 277 | }); | |
| 278 | ||
| 279 | await vi.waitFor(() => { | |
| 280 | expect(environment.terminal.open).toHaveBeenCalledTimes(1); | |
| 281 | }); | |
| 282 | expect(terminalDisposeSpy).not.toHaveBeenCalled(); | |
| 283 | } finally { | |
| 284 | await mounted.cleanup(); | |
| 285 | } | |
| 286 | }); | |
| 287 | ||
| 262 | 288 | it("uses the drawer surface colors for the terminal theme", async () => { |
| 263 | 289 | const environment = createEnvironmentApi(); |
| 264 | 290 | environmentApiById.set("environment-a", environment); |
apps/web/src/components/ThreadTerminalDrawer.tsx+5-4| ... | ... | @@ -268,6 +268,7 @@ export function TerminalViewport({ |
| 268 | 268 | const containerRef = useRef<HTMLDivElement>(null); |
| 269 | 269 | const terminalRef = useRef<Terminal | null>(null); |
| 270 | 270 | const fitAddonRef = useRef<FitAddon | null>(null); |
| 271 | const environmentId = threadRef.environmentId; | |
| 271 | 272 | const hasHandledExitRef = useRef(false); |
| 272 | 273 | const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); |
| 273 | 274 | const selectionGestureActiveRef = useRef(false); |
| ... | ... | @@ -289,7 +290,7 @@ export function TerminalViewport({ |
| 289 | 290 | if (!mount) return; |
| 290 | 291 | |
| 291 | 292 | let disposed = false; |
| 292 | const api = readEnvironmentApi(threadRef.environmentId); | |
| 293 | const api = readEnvironmentApi(environmentId); | |
| 293 | 294 | const localApi = readLocalApi(); |
| 294 | 295 | if (!api || !localApi) return; |
| 295 | 296 | |
| ... | ... | @@ -706,7 +707,7 @@ export function TerminalViewport({ |
| 706 | 707 | // autoFocus is intentionally omitted; |
| 707 | 708 | // it is only read at mount time and must not trigger terminal teardown/recreation. |
| 708 | 709 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 709 | }, [cwd, runtimeEnv, terminalId, threadId, threadRef]); | |
| 710 | }, [cwd, environmentId, runtimeEnv, terminalId, threadId]); | |
| 710 | 711 | |
| 711 | 712 | useEffect(() => { |
| 712 | 713 | if (!autoFocus) return; |
| ... | ... | @@ -721,7 +722,7 @@ export function TerminalViewport({ |
| 721 | 722 | }, [autoFocus, focusRequestId]); |
| 722 | 723 | |
| 723 | 724 | useEffect(() => { |
| 724 | const api = readEnvironmentApi(threadRef.environmentId); | |
| 725 | const api = readEnvironmentApi(environmentId); | |
| 725 | 726 | const terminal = terminalRef.current; |
| 726 | 727 | const fitAddon = fitAddonRef.current; |
| 727 | 728 | if (!api || !terminal || !fitAddon) return; |
| ... | ... | @@ -743,7 +744,7 @@ export function TerminalViewport({ |
| 743 | 744 | return () => { |
| 744 | 745 | window.cancelAnimationFrame(frame); |
| 745 | 746 | }; |
| 746 | }, [drawerHeight, resizeEpoch, terminalId, threadId, threadRef]); | |
| 747 | }, [drawerHeight, environmentId, resizeEpoch, terminalId, threadId]); | |
| 747 | 748 | return ( |
| 748 | 749 | <div |
| 749 | 750 | ref={containerRef} |
apps/web/src/components/settings/ConnectionsSettings.tsx+3-12| ... | ... | @@ -52,9 +52,10 @@ import { |
| 52 | 52 | revokeOtherServerClientSessions, |
| 53 | 53 | revokeServerClientSession, |
| 54 | 54 | revokeServerPairingLink, |
| 55 | isLoopbackHostname, | |
| 55 | 56 | type ServerClientSessionRecord, |
| 56 | 57 | type ServerPairingLinkRecord, |
| 57 | } from "../../environments/primary"; | |
| 58 | } from "~/environments/primary"; | |
| 58 | 59 | import type { WsRpcClient } from "~/rpc/wsRpcClient"; |
| 59 | 60 | import { |
| 60 | 61 | type SavedEnvironmentRecord, |
| ... | ... | @@ -65,7 +66,7 @@ import { |
| 65 | 66 | getPrimaryEnvironmentConnection, |
| 66 | 67 | reconnectSavedEnvironment, |
| 67 | 68 | removeSavedEnvironment, |
| 68 | } from "../../environments/runtime"; | |
| 69 | } from "~/environments/runtime"; | |
| 69 | 70 | |
| 70 | 71 | const accessTimestampFormatter = new Intl.DateTimeFormat(undefined, { |
| 71 | 72 | dateStyle: "medium", |
| ... | ... | @@ -253,16 +254,6 @@ function resolveCurrentOriginPairingUrl(credential: string): string { |
| 253 | 254 | return setPairingTokenOnUrl(url, credential).toString(); |
| 254 | 255 | } |
| 255 | 256 | |
| 256 | function isLoopbackHostname(hostname: string): boolean { | |
| 257 | const normalized = hostname.trim().toLowerCase(); | |
| 258 | return ( | |
| 259 | normalized === "localhost" || | |
| 260 | normalized === "127.0.0.1" || | |
| 261 | normalized === "::1" || | |
| 262 | normalized === "[::1]" | |
| 263 | ); | |
| 264 | } | |
| 265 | ||
| 266 | 257 | type PairingLinkListRowProps = { |
| 267 | 258 | pairingLink: ServerPairingLinkRecord; |
| 268 | 259 | endpointUrl: string | null | undefined; |
apps/web/src/environments/primary/bootstrap.test.ts+23| ... | ... | @@ -118,4 +118,27 @@ describe("environmentBootstrap", () => { |
| 118 | 118 | await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT); |
| 119 | 119 | expect(fetchMock).toHaveBeenCalledWith("http://localhost:5735/.well-known/t3/environment"); |
| 120 | 120 | }); |
| 121 | ||
| 122 | it("uses the vite proxy for desktop-managed loopback descriptor requests during local dev", async () => { | |
| 123 | const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(BASE_ENVIRONMENT)); | |
| 124 | vi.stubGlobal("fetch", fetchMock); | |
| 125 | vi.stubEnv("VITE_DEV_SERVER_URL", "http://127.0.0.1:5733"); | |
| 126 | vi.stubGlobal("window", { | |
| 127 | location: new URL("http://127.0.0.1:5733/"), | |
| 128 | history: { | |
| 129 | replaceState: vi.fn(), | |
| 130 | }, | |
| 131 | desktopBridge: { | |
| 132 | getLocalEnvironmentBootstrap: () => ({ | |
| 133 | label: "Local environment", | |
| 134 | httpBaseUrl: "http://127.0.0.1:3773", | |
| 135 | wsBaseUrl: "ws://127.0.0.1:3773", | |
| 136 | bootstrapToken: "desktop-bootstrap-token", | |
| 137 | }), | |
| 138 | }, | |
| 139 | }); | |
| 140 | ||
| 141 | await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT); | |
| 142 | expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:5733/.well-known/t3/environment"); | |
| 143 | }); | |
| 121 | 144 | }); |
apps/web/src/environments/primary/index.ts+1-1| ... | ... | @@ -32,4 +32,4 @@ export { |
| 32 | 32 | __resetServerAuthBootstrapForTests, |
| 33 | 33 | } from "./auth"; |
| 34 | 34 | |
| 35 | export { resolvePrimaryEnvironmentHttpUrl } from "./target"; | |
| 35 | export { resolvePrimaryEnvironmentHttpUrl, isLoopbackHostname } from "./target"; |
apps/web/src/environments/primary/target.ts+40-1| ... | ... | @@ -6,6 +6,8 @@ export interface PrimaryEnvironmentTarget { |
| 6 | 6 | readonly target: KnownEnvironment["target"]; |
| 7 | 7 | } |
| 8 | 8 | |
| 9 | const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]); | |
| 10 | ||
| 9 | 11 | function getDesktopLocalEnvironmentBootstrap(): DesktopEnvironmentBootstrap | null { |
| 10 | 12 | return window.desktopBridge?.getLocalEnvironmentBootstrap() ?? null; |
| 11 | 13 | } |
| ... | ... | @@ -14,6 +16,43 @@ function normalizeBaseUrl(rawValue: string): string { |
| 14 | 16 | return new URL(rawValue, window.location.origin).toString(); |
| 15 | 17 | } |
| 16 | 18 | |
| 19 | function normalizeHostname(hostname: string): string { | |
| 20 | return hostname | |
| 21 | .trim() | |
| 22 | .toLowerCase() | |
| 23 | .replace(/^\[(.*)\]$/, "$1"); | |
| 24 | } | |
| 25 | ||
| 26 | export function isLoopbackHostname(hostname: string): boolean { | |
| 27 | return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname)); | |
| 28 | } | |
| 29 | ||
| 30 | function resolveHttpRequestBaseUrl(httpBaseUrl: string): string { | |
| 31 | const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim(); | |
| 32 | if (!configuredDevServerUrl) { | |
| 33 | return httpBaseUrl; | |
| 34 | } | |
| 35 | ||
| 36 | const currentUrl = new URL(window.location.href); | |
| 37 | const targetUrl = new URL(httpBaseUrl); | |
| 38 | const devServerUrl = new URL(configuredDevServerUrl, currentUrl.origin); | |
| 39 | ||
| 40 | const isCurrentOriginDevServer = | |
| 41 | (currentUrl.protocol === "http:" || currentUrl.protocol === "https:") && | |
| 42 | currentUrl.origin === devServerUrl.origin; | |
| 43 | ||
| 44 | if ( | |
| 45 | !isCurrentOriginDevServer || | |
| 46 | currentUrl.origin === targetUrl.origin || | |
| 47 | !isLoopbackHostname(currentUrl.hostname) || | |
| 48 | !isLoopbackHostname(targetUrl.hostname) | |
| 49 | ) { | |
| 50 | return httpBaseUrl; | |
| 51 | } | |
| 52 | ||
| 53 | return currentUrl.origin; | |
| 54 | } | |
| 55 | ||
| 17 | 56 | function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null { |
| 18 | 57 | const configuredHttpBaseUrl = import.meta.env.VITE_HTTP_URL?.trim(); |
| 19 | 58 | const configuredWsBaseUrl = import.meta.env.VITE_WS_URL?.trim(); |
| ... | ... | @@ -86,7 +125,7 @@ export function resolvePrimaryEnvironmentHttpUrl( |
| 86 | 125 | throw new Error("Unable to resolve the primary environment HTTP base URL."); |
| 87 | 126 | } |
| 88 | 127 | |
| 89 | const url = new URL(primaryTarget.target.httpBaseUrl); | |
| 128 | const url = new URL(resolveHttpRequestBaseUrl(primaryTarget.target.httpBaseUrl)); | |
| 90 | 129 | url.pathname = pathname; |
| 91 | 130 | if (searchParams) { |
| 92 | 131 | url.search = new URLSearchParams(searchParams).toString(); |
apps/web/src/environments/runtime/service.test.ts created+41| ... | ... | @@ -0,0 +1,41 @@ |
| 1 | import { describe, expect, it } from "vitest"; | |
| 2 | ||
| 3 | import { shouldApplyTerminalEvent } from "./service"; | |
| 4 | ||
| 5 | describe("shouldApplyTerminalEvent", () => { | |
| 6 | it("applies terminal events for draft-only threads", () => { | |
| 7 | expect( | |
| 8 | shouldApplyTerminalEvent({ | |
| 9 | serverThreadArchivedAt: undefined, | |
| 10 | hasDraftThread: true, | |
| 11 | }), | |
| 12 | ).toBe(true); | |
| 13 | }); | |
| 14 | ||
| 15 | it("drops terminal events for unknown threads", () => { | |
| 16 | expect( | |
| 17 | shouldApplyTerminalEvent({ | |
| 18 | serverThreadArchivedAt: undefined, | |
| 19 | hasDraftThread: false, | |
| 20 | }), | |
| 21 | ).toBe(false); | |
| 22 | }); | |
| 23 | ||
| 24 | it("drops terminal events for archived server threads even if a draft exists", () => { | |
| 25 | expect( | |
| 26 | shouldApplyTerminalEvent({ | |
| 27 | serverThreadArchivedAt: "2026-04-09T00:00:00.000Z", | |
| 28 | hasDraftThread: true, | |
| 29 | }), | |
| 30 | ).toBe(false); | |
| 31 | }); | |
| 32 | ||
| 33 | it("applies terminal events for active server threads", () => { | |
| 34 | expect( | |
| 35 | shouldApplyTerminalEvent({ | |
| 36 | serverThreadArchivedAt: null, | |
| 37 | hasDraftThread: false, | |
| 38 | }), | |
| 39 | ).toBe(true); | |
| 40 | }); | |
| 41 | }); |
apps/web/src/environments/runtime/service.ts+20-2| ... | ... | @@ -196,6 +196,17 @@ function reconcileSnapshotDerivedState() { |
| 196 | 196 | useTerminalStateStore.getState().removeOrphanedTerminalStates(activeThreadKeys); |
| 197 | 197 | } |
| 198 | 198 | |
| 199 | export function shouldApplyTerminalEvent(input: { | |
| 200 | serverThreadArchivedAt: string | null | undefined; | |
| 201 | hasDraftThread: boolean; | |
| 202 | }): boolean { | |
| 203 | if (input.serverThreadArchivedAt !== undefined) { | |
| 204 | return input.serverThreadArchivedAt === null; | |
| 205 | } | |
| 206 | ||
| 207 | return input.hasDraftThread; | |
| 208 | } | |
| 209 | ||
| 199 | 210 | function applyRecoveredEventBatch( |
| 200 | 211 | events: ReadonlyArray<OrchestrationEvent>, |
| 201 | 212 | environmentId: EnvironmentId, |
| ... | ... | @@ -266,8 +277,15 @@ function createEnvironmentConnectionHandlers() { |
| 266 | 277 | }, |
| 267 | 278 | applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => { |
| 268 | 279 | const threadRef = scopeThreadRef(environmentId, ThreadId.makeUnsafe(event.threadId)); |
| 269 | const thread = selectThreadByRef(useStore.getState(), threadRef); | |
| 270 | if (!thread || thread.archivedAt !== null) { | |
| 280 | const serverThread = selectThreadByRef(useStore.getState(), threadRef); | |
| 281 | const hasDraftThread = | |
| 282 | useComposerDraftStore.getState().getDraftThreadByRef(threadRef) !== null; | |
| 283 | if ( | |
| 284 | !shouldApplyTerminalEvent({ | |
| 285 | serverThreadArchivedAt: serverThread?.archivedAt, | |
| 286 | hasDraftThread, | |
| 287 | }) | |
| 288 | ) { | |
| 271 | 289 | return; |
| 272 | 290 | } |
| 273 | 291 | useTerminalStateStore.getState().applyTerminalEvent(threadRef, event); |
apps/web/vite.config.ts+4| ... | ... | @@ -72,6 +72,10 @@ export default defineConfig({ |
| 72 | 72 | ...(devProxyTarget |
| 73 | 73 | ? { |
| 74 | 74 | proxy: { |
| 75 | "/.well-known": { | |
| 76 | target: devProxyTarget, | |
| 77 | changeOrigin: true, | |
| 78 | }, | |
| 75 | 79 | "/api": { |
| 76 | 80 | target: devProxyTarget, |
| 77 | 81 | changeOrigin: true, |