authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-09 10:53:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-09 17:53:46+00:00
log5b3b31b6e6846eb0509fb2e7c31646ae843c5609
tree289c883b2ee04e7c7902db4ae37835048b595266
parentb7559c467803f923e59a287c112b4c92b96e15c7
signature Signed by PGP key B5690EEEBB952194

Use dev proxy for loopback auth and environment requests (#1853)


11 files changed, 231 insertions(+), 45 deletions(-)

apps/web/src/authBootstrap.test.ts+41
...@@ -175,6 +175,47 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -175,6 +175,47 @@ describe("resolveInitialServerAuthGateState", () => {
175 });175 });
176 });176 });
177177
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 it("returns a requires-auth state instead of throwing when no bootstrap credential exists", async () => {219 it("returns a requires-auth state instead of throwing when no bootstrap credential exists", async () => {
179 const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(220 const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
180 sessionResponse({221 sessionResponse({
apps/web/src/components/ChatView.tsx+27-25
...@@ -31,7 +31,7 @@ import {...@@ -31,7 +31,7 @@ import {
31import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model";31import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model";
32import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";32import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
33import { truncate } from "@t3tools/shared/String";33import { truncate } from "@t3tools/shared/String";
34import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";34import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
35import { useQuery } from "@tanstack/react-query";35import { useQuery } from "@tanstack/react-query";
36import { useDebouncedValue } from "@tanstack/react-pacer";36import { useDebouncedValue } from "@tanstack/react-pacer";
37import { useNavigate, useSearch } from "@tanstack/react-router";37import { useNavigate, useSearch } from "@tanstack/react-router";
...@@ -491,7 +491,7 @@ interface PersistentThreadTerminalDrawerProps {...@@ -491,7 +491,7 @@ interface PersistentThreadTerminalDrawerProps {
491 onAddTerminalContext: (selection: TerminalContextSelection) => void;491 onAddTerminalContext: (selection: TerminalContextSelection) => void;
492}492}
493493
494function PersistentThreadTerminalDrawer({494const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDrawer({
495 threadRef,495 threadRef,
496 threadId,496 threadId,
497 visible,497 visible,
...@@ -650,7 +650,7 @@ function PersistentThreadTerminalDrawer({...@@ -650,7 +650,7 @@ function PersistentThreadTerminalDrawer({
650 />650 />
651 </div>651 </div>
652 );652 );
653}653});
654654
655export default function ChatView(props: ChatViewProps) {655export default function ChatView(props: ChatViewProps) {
656 const { environmentId, threadId, routeKind } = props;656 const { environmentId, threadId, routeKind } = props;
...@@ -867,6 +867,14 @@ export default function ChatView(props: ChatViewProps) {...@@ -867,6 +867,14 @@ export default function ChatView(props: ChatViewProps) {
867 [draftThreadsByThreadKey],867 [draftThreadsByThreadKey],
868 );868 );
869 const [mountedTerminalThreadKeys, setMountedTerminalThreadKeys] = useState<string[]>([]);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 );
870878
871 const setPrompt = useCallback(879 const setPrompt = useCallback(
872 (nextPrompt: string) => {880 (nextPrompt: string) => {
...@@ -4850,28 +4858,22 @@ export default function ChatView(props: ChatViewProps) {...@@ -4850,28 +4858,22 @@ export default function ChatView(props: ChatViewProps) {
4850 </div>4858 </div>
4851 {/* end horizontal flex container */}4859 {/* end horizontal flex container */}
48524860
4853 {mountedTerminalThreadKeys.flatMap((mountedThreadKey) => {4861 {mountedTerminalThreadRefs.map(({ key: mountedThreadKey, threadRef: mountedThreadRef }) => (
4854 const mountedThreadRef = parseScopedThreadKey(mountedThreadKey);4862 <PersistentThreadTerminalDrawer
4855 if (!mountedThreadRef) {4863 key={mountedThreadKey}
4856 return [];4864 threadRef={mountedThreadRef}
4857 }4865 threadId={mountedThreadRef.threadId}
4858 return [4866 visible={mountedThreadKey === activeThreadKey && terminalState.terminalOpen}
4859 <PersistentThreadTerminalDrawer4867 launchContext={
4860 key={mountedThreadKey}4868 mountedThreadKey === activeThreadKey ? (activeTerminalLaunchContext ?? null) : null
4861 threadRef={mountedThreadRef}4869 }
4862 threadId={mountedThreadRef.threadId}4870 focusRequestId={mountedThreadKey === activeThreadKey ? terminalFocusRequestId : 0}
4863 visible={mountedThreadKey === activeThreadKey && terminalState.terminalOpen}4871 splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
4864 launchContext={4872 newShortcutLabel={newTerminalShortcutLabel ?? undefined}
4865 mountedThreadKey === activeThreadKey ? (activeTerminalLaunchContext ?? null) : null4873 closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
4866 }4874 onAddTerminalContext={addTerminalContextToDraft}
4867 focusRequestId={mountedThreadKey === activeThreadKey ? terminalFocusRequestId : 0}4875 />
4868 splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}4876 ))}
4869 newShortcutLabel={newTerminalShortcutLabel ?? undefined}
4870 closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
4871 onAddTerminalContext={addTerminalContextToDraft}
4872 />,
4873 ];
4874 })}
48754877
4876 {expandedImage && expandedImageItem && (4878 {expandedImage && expandedImageItem && (
4877 <div4879 <div
apps/web/src/components/ThreadTerminalDrawer.browser.tsx+26
...@@ -259,6 +259,32 @@ describe("TerminalViewport", () => {...@@ -259,6 +259,32 @@ describe("TerminalViewport", () => {
259 }259 }
260 });260 });
261261
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 it("uses the drawer surface colors for the terminal theme", async () => {288 it("uses the drawer surface colors for the terminal theme", async () => {
263 const environment = createEnvironmentApi();289 const environment = createEnvironmentApi();
264 environmentApiById.set("environment-a", environment);290 environmentApiById.set("environment-a", environment);
apps/web/src/components/ThreadTerminalDrawer.tsx+5-4
...@@ -268,6 +268,7 @@ export function TerminalViewport({...@@ -268,6 +268,7 @@ export function TerminalViewport({
268 const containerRef = useRef<HTMLDivElement>(null);268 const containerRef = useRef<HTMLDivElement>(null);
269 const terminalRef = useRef<Terminal | null>(null);269 const terminalRef = useRef<Terminal | null>(null);
270 const fitAddonRef = useRef<FitAddon | null>(null);270 const fitAddonRef = useRef<FitAddon | null>(null);
271 const environmentId = threadRef.environmentId;
271 const hasHandledExitRef = useRef(false);272 const hasHandledExitRef = useRef(false);
272 const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);273 const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);
273 const selectionGestureActiveRef = useRef(false);274 const selectionGestureActiveRef = useRef(false);
...@@ -289,7 +290,7 @@ export function TerminalViewport({...@@ -289,7 +290,7 @@ export function TerminalViewport({
289 if (!mount) return;290 if (!mount) return;
290291
291 let disposed = false;292 let disposed = false;
292 const api = readEnvironmentApi(threadRef.environmentId);293 const api = readEnvironmentApi(environmentId);
293 const localApi = readLocalApi();294 const localApi = readLocalApi();
294 if (!api || !localApi) return;295 if (!api || !localApi) return;
295296
...@@ -706,7 +707,7 @@ export function TerminalViewport({...@@ -706,7 +707,7 @@ export function TerminalViewport({
706 // autoFocus is intentionally omitted;707 // autoFocus is intentionally omitted;
707 // it is only read at mount time and must not trigger terminal teardown/recreation.708 // it is only read at mount time and must not trigger terminal teardown/recreation.
708 // eslint-disable-next-line react-hooks/exhaustive-deps709 // eslint-disable-next-line react-hooks/exhaustive-deps
709 }, [cwd, runtimeEnv, terminalId, threadId, threadRef]);710 }, [cwd, environmentId, runtimeEnv, terminalId, threadId]);
710711
711 useEffect(() => {712 useEffect(() => {
712 if (!autoFocus) return;713 if (!autoFocus) return;
...@@ -721,7 +722,7 @@ export function TerminalViewport({...@@ -721,7 +722,7 @@ export function TerminalViewport({
721 }, [autoFocus, focusRequestId]);722 }, [autoFocus, focusRequestId]);
722723
723 useEffect(() => {724 useEffect(() => {
724 const api = readEnvironmentApi(threadRef.environmentId);725 const api = readEnvironmentApi(environmentId);
725 const terminal = terminalRef.current;726 const terminal = terminalRef.current;
726 const fitAddon = fitAddonRef.current;727 const fitAddon = fitAddonRef.current;
727 if (!api || !terminal || !fitAddon) return;728 if (!api || !terminal || !fitAddon) return;
...@@ -743,7 +744,7 @@ export function TerminalViewport({...@@ -743,7 +744,7 @@ export function TerminalViewport({
743 return () => {744 return () => {
744 window.cancelAnimationFrame(frame);745 window.cancelAnimationFrame(frame);
745 };746 };
746 }, [drawerHeight, resizeEpoch, terminalId, threadId, threadRef]);747 }, [drawerHeight, environmentId, resizeEpoch, terminalId, threadId]);
747 return (748 return (
748 <div749 <div
749 ref={containerRef}750 ref={containerRef}
apps/web/src/components/settings/ConnectionsSettings.tsx+3-12
...@@ -52,9 +52,10 @@ import {...@@ -52,9 +52,10 @@ import {
52 revokeOtherServerClientSessions,52 revokeOtherServerClientSessions,
53 revokeServerClientSession,53 revokeServerClientSession,
54 revokeServerPairingLink,54 revokeServerPairingLink,
55 isLoopbackHostname,
55 type ServerClientSessionRecord,56 type ServerClientSessionRecord,
56 type ServerPairingLinkRecord,57 type ServerPairingLinkRecord,
57} from "../../environments/primary";58} from "~/environments/primary";
58import type { WsRpcClient } from "~/rpc/wsRpcClient";59import type { WsRpcClient } from "~/rpc/wsRpcClient";
59import {60import {
60 type SavedEnvironmentRecord,61 type SavedEnvironmentRecord,
...@@ -65,7 +66,7 @@ import {...@@ -65,7 +66,7 @@ import {
65 getPrimaryEnvironmentConnection,66 getPrimaryEnvironmentConnection,
66 reconnectSavedEnvironment,67 reconnectSavedEnvironment,
67 removeSavedEnvironment,68 removeSavedEnvironment,
68} from "../../environments/runtime";69} from "~/environments/runtime";
6970
70const accessTimestampFormatter = new Intl.DateTimeFormat(undefined, {71const accessTimestampFormatter = new Intl.DateTimeFormat(undefined, {
71 dateStyle: "medium",72 dateStyle: "medium",
...@@ -253,16 +254,6 @@ function resolveCurrentOriginPairingUrl(credential: string): string {...@@ -253,16 +254,6 @@ function resolveCurrentOriginPairingUrl(credential: string): string {
253 return setPairingTokenOnUrl(url, credential).toString();254 return setPairingTokenOnUrl(url, credential).toString();
254}255}
255256
256function 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
266type PairingLinkListRowProps = {257type PairingLinkListRowProps = {
267 pairingLink: ServerPairingLinkRecord;258 pairingLink: ServerPairingLinkRecord;
268 endpointUrl: string | null | undefined;259 endpointUrl: string | null | undefined;
apps/web/src/environments/primary/bootstrap.test.ts+23
...@@ -118,4 +118,27 @@ describe("environmentBootstrap", () => {...@@ -118,4 +118,27 @@ describe("environmentBootstrap", () => {
118 await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT);118 await expect(resolveInitialPrimaryEnvironmentDescriptor()).resolves.toEqual(BASE_ENVIRONMENT);
119 expect(fetchMock).toHaveBeenCalledWith("http://localhost:5735/.well-known/t3/environment");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,4 +32,4 @@ export {
32 __resetServerAuthBootstrapForTests,32 __resetServerAuthBootstrapForTests,
33} from "./auth";33} from "./auth";
3434
35export { resolvePrimaryEnvironmentHttpUrl } from "./target";35export { resolvePrimaryEnvironmentHttpUrl, isLoopbackHostname } from "./target";
apps/web/src/environments/primary/target.ts+40-1
...@@ -6,6 +6,8 @@ export interface PrimaryEnvironmentTarget {...@@ -6,6 +6,8 @@ export interface PrimaryEnvironmentTarget {
6 readonly target: KnownEnvironment["target"];6 readonly target: KnownEnvironment["target"];
7}7}
88
9const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
10
9function getDesktopLocalEnvironmentBootstrap(): DesktopEnvironmentBootstrap | null {11function getDesktopLocalEnvironmentBootstrap(): DesktopEnvironmentBootstrap | null {
10 return window.desktopBridge?.getLocalEnvironmentBootstrap() ?? null;12 return window.desktopBridge?.getLocalEnvironmentBootstrap() ?? null;
11}13}
...@@ -14,6 +16,43 @@ function normalizeBaseUrl(rawValue: string): string {...@@ -14,6 +16,43 @@ function normalizeBaseUrl(rawValue: string): string {
14 return new URL(rawValue, window.location.origin).toString();16 return new URL(rawValue, window.location.origin).toString();
15}17}
1618
19function normalizeHostname(hostname: string): string {
20 return hostname
21 .trim()
22 .toLowerCase()
23 .replace(/^\[(.*)\]$/, "$1");
24}
25
26export function isLoopbackHostname(hostname: string): boolean {
27 return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
28}
29
30function 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
17function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null {56function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null {
18 const configuredHttpBaseUrl = import.meta.env.VITE_HTTP_URL?.trim();57 const configuredHttpBaseUrl = import.meta.env.VITE_HTTP_URL?.trim();
19 const configuredWsBaseUrl = import.meta.env.VITE_WS_URL?.trim();58 const configuredWsBaseUrl = import.meta.env.VITE_WS_URL?.trim();
...@@ -86,7 +125,7 @@ export function resolvePrimaryEnvironmentHttpUrl(...@@ -86,7 +125,7 @@ export function resolvePrimaryEnvironmentHttpUrl(
86 throw new Error("Unable to resolve the primary environment HTTP base URL.");125 throw new Error("Unable to resolve the primary environment HTTP base URL.");
87 }126 }
88127
89 const url = new URL(primaryTarget.target.httpBaseUrl);128 const url = new URL(resolveHttpRequestBaseUrl(primaryTarget.target.httpBaseUrl));
90 url.pathname = pathname;129 url.pathname = pathname;
91 if (searchParams) {130 if (searchParams) {
92 url.search = new URLSearchParams(searchParams).toString();131 url.search = new URLSearchParams(searchParams).toString();
apps/web/src/environments/runtime/service.test.ts created+41
...@@ -0,0 +1,41 @@
1import { describe, expect, it } from "vitest";
2
3import { shouldApplyTerminalEvent } from "./service";
4
5describe("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,6 +196,17 @@ function reconcileSnapshotDerivedState() {
196 useTerminalStateStore.getState().removeOrphanedTerminalStates(activeThreadKeys);196 useTerminalStateStore.getState().removeOrphanedTerminalStates(activeThreadKeys);
197}197}
198198
199export 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
199function applyRecoveredEventBatch(210function applyRecoveredEventBatch(
200 events: ReadonlyArray<OrchestrationEvent>,211 events: ReadonlyArray<OrchestrationEvent>,
201 environmentId: EnvironmentId,212 environmentId: EnvironmentId,
...@@ -266,8 +277,15 @@ function createEnvironmentConnectionHandlers() {...@@ -266,8 +277,15 @@ function createEnvironmentConnectionHandlers() {
266 },277 },
267 applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => {278 applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => {
268 const threadRef = scopeThreadRef(environmentId, ThreadId.makeUnsafe(event.threadId));279 const threadRef = scopeThreadRef(environmentId, ThreadId.makeUnsafe(event.threadId));
269 const thread = selectThreadByRef(useStore.getState(), threadRef);280 const serverThread = selectThreadByRef(useStore.getState(), threadRef);
270 if (!thread || thread.archivedAt !== null) {281 const hasDraftThread =
282 useComposerDraftStore.getState().getDraftThreadByRef(threadRef) !== null;
283 if (
284 !shouldApplyTerminalEvent({
285 serverThreadArchivedAt: serverThread?.archivedAt,
286 hasDraftThread,
287 })
288 ) {
271 return;289 return;
272 }290 }
273 useTerminalStateStore.getState().applyTerminalEvent(threadRef, event);291 useTerminalStateStore.getState().applyTerminalEvent(threadRef, event);
apps/web/vite.config.ts+4
...@@ -72,6 +72,10 @@ export default defineConfig({...@@ -72,6 +72,10 @@ export default defineConfig({
72 ...(devProxyTarget72 ...(devProxyTarget
73 ? {73 ? {
74 proxy: {74 proxy: {
75 "/.well-known": {
76 target: devProxyTarget,
77 changeOrigin: true,
78 },
75 "/api": {79 "/api": {
76 target: devProxyTarget,80 target: devProxyTarget,
77 changeOrigin: true,81 changeOrigin: true,