Repository navigation
fix(web): agent browser tabs keep your sign-ins when you're in another thread #21938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,28 @@ | ||
| "use client"; | ||
|
|
||
| import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; | ||
| import { | ||
| parseScopedThreadKey, | ||
| scopedThreadKey, | ||
| scopeThreadRef, | ||
| } from "@t3tools/client-runtime/environment"; | ||
| import { | ||
| AuthPreviewOperateScope, | ||
| type DesktopPreviewOpenLinkEvent, | ||
| type EnvironmentId, | ||
| FILL_PREVIEW_VIEWPORT, | ||
| type ScopedThreadRef, | ||
| ThreadId, | ||
| } from "@t3tools/contracts"; | ||
| import { useAtomValue } from "@effect/atom-react"; | ||
| import { AsyncResult, Atom } from "effect/reactivity"; | ||
| import { type ComponentProps, useEffect, useEffectEvent, useState } from "react"; | ||
|
|
||
| import { primaryEnvironmentIdAtom } from "~/state/primaryEnvironment"; | ||
|
|
||
| import { isElectron } from "~/env"; | ||
| import { useClientSettingsHydrated } from "~/hooks/useSettings"; | ||
| import { useActivePreviewSessions } from "~/previewStateStore"; | ||
| import { usePreviewSession } from "~/components/preview/usePreviewSession"; | ||
| import { applyPreviewServerEvent, useActivePreviewSessions } from "~/previewStateStore"; | ||
| import { previewEnvironment } from "~/state/preview"; | ||
| import { useEnvironmentScope } from "~/state/session"; | ||
| import { useAtomCommand } from "~/state/use-atom-command"; | ||
|
|
@@ -26,9 +35,40 @@ import { openUrlInPreview } from "./openFileInPreview"; | |
| import { rendersServerTabNatively } from "./previewRuntime"; | ||
| import { previewRuntimeTabId } from "./previewRuntimeTabId"; | ||
|
|
||
| /** | ||
| * The server binds a tab to this desktop's browser profile only if the desktop attaches it within | ||
| * seconds of opening; otherwise the tab runs in the server's own browser, without the user's | ||
| * sign-ins. Agents open tabs in threads the user is not viewing, so every server tab opened in the | ||
| * primary environment is seeded here instead of waiting for its thread's view to subscribe. | ||
| */ | ||
| const serverTabOpeningsAtom = Atom.family((environmentId: EnvironmentId) => { | ||
| const events = previewEnvironment.events({ environmentId, input: {} }); | ||
| return Atom.make((get) => { | ||
| get.subscribe(events, (result) => { | ||
| if (!AsyncResult.isSuccess(result)) return; | ||
| const event = result.value; | ||
| if (event.type !== "opened" || event.snapshot.runtime !== "server") return; | ||
| applyPreviewServerEvent(scopeThreadRef(environmentId, ThreadId.make(event.threadId)), event); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '35,78p' apps/web/src/browser/ElectronBrowserHost.tsx
sed -n '145,240p' apps/web/src/previewStateStore.ts
sed -n '288,367p' apps/web/src/previewStateStore.ts
sed -n '25,100p' apps/web/src/components/preview/usePreviewSession.tsRepository: pingdotgg/t3code Length of output: 11725 🏁 Script executed: set -o pipefail
printf '%s\n' '--- PR diff ---'
git diff --stat b34501a466d9e6302c41c88a69eca2770a5c5995 913258306859527053e6bc6c68674b6fd5eabe13 -- apps/web/src/browser/ElectronBrowserHost.tsx apps/web/src/previewStateStore.ts apps/web/src/components/preview/usePreviewSession.ts
git diff --unified=80 b34501a466d9e6302c41c88a69eca2770a5c5995 913258306859527053e6bc6c68674b6fd5eabe13 -- apps/web/src/browser/ElectronBrowserHost.tsx apps/web/src/previewStateStore.ts apps/web/src/components/preview/usePreviewSession.ts
printf '%s\n' '--- epoch/revision declarations and producers ---'
rg -n -F --glob '*.{ts,tsx}' -e 'serverEpoch' -e 'serverRevision' -e 'PreviewEvent' -e 'epoch' apps/web/src apps/server/src || test "$?" -eq 1
printf '%s\n' '--- preview event/list implementation references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'previewEnvironment.events' -e 'previewEnvironment.list' -e 'events({' -e 'server-runtime' apps/web/src apps/server/src || test "$?" -eq 1Repository: pingdotgg/t3code Length of output: 31844 🏁 Script executed: set -o pipefail
printf '%s\n' '--- manager epoch and event lifecycle ---'
sed -n '1,235p' apps/server/src/preview/Manager.ts
sed -n '250,455p' apps/server/src/preview/Manager.ts
printf '%s\n' '--- preview contract declarations ---'
rg -n -F --glob '*.{ts,tsx}' -e 'export type PreviewEvent' -e 'interface PreviewEvent' -e 'PreviewListResult' -e 'subscribePreviewEvents' -e 'previewEvents' packages apps | head -120
printf '%s\n' '--- epoch transition tests and relevant sync tests ---'
sed -n '400,610p' apps/web/src/previewStateStore.test.ts
sed -n '1,240p' apps/server/src/preview/Manager.test.ts
sed -n '1,250p' apps/web/src/components/preview/previewSessionSync.browser.test.tsRepository: pingdotgg/t3code Length of output: 42895 🏁 Script executed: set -o pipefail
printf '%s\n' '--- client preview state and RPC atom binding ---'
sed -n '1,180p' packages/client-runtime/src/state/preview.ts
sed -n '240,335p' packages/contracts/src/preview.ts
sed -n '1,130p' apps/web/src/state/preview.ts
printf '%s\n' '--- preview manager layer and server lifecycle references ---'
rg -n -F --glob '*.ts' -e 'PreviewManager.layer' -e 'PreviewManager.make' -e 'previewManager' -e 'layerPreview' apps/server/src | head -160
sed -n '2840,2895p' apps/server/src/ws.ts
printf '%s\n' '--- restart/reconnect handling in client runtime ---'
rg -n -F --glob '*.{ts,tsx}' -e 'reconnect' -e 'connection' -e 'AsyncResult' packages/client-runtime/src apps/web/src/state apps/web/src/rpc | head -200Repository: pingdotgg/t3code Length of output: 36686 🏁 Script executed: set -o pipefail
printf '%s\n' '--- subscription atom implementation ---'
rg -n -F --glob '*.ts' -e 'createEnvironmentRpcSubscriptionAtomFamily' -e 'function createEnvironmentRpcSubscriptionAtomFamily' packages/client-runtime/src
sed -n '1,280p' packages/client-runtime/src/state/runtime.ts
printf '%s\n' '--- connection/session stream lifecycle ---'
rg -n -F --glob '*.ts' -e 'createRpc' -e 'RpcClient' -e 'Stream.run' -e 'disconnect' -e 'connectionAtomRuntime' packages/client-runtime/src/connection packages/client-runtime/src/state/runtime.ts | head -180Repository: pingdotgg/t3code Length of output: 18733 🏁 Script executed: set -o pipefail
sed -n '730,890p' packages/client-runtime/src/state/runtime.ts
sed -n '1,260p' packages/client-runtime/src/state/environmentStreams.tsRepository: pingdotgg/t3code Length of output: 7256 🏁 Script executed: set -o pipefail
rg -n -F --glob '*.ts' -e 'function createEnvironmentSubscriptionAtomFamily' -e 'createEnvironmentSubscriptionAtomFamily' -e 'followStream(' packages/client-runtime/src/state/runtime.ts packages/client-runtime/src/connection/registry.ts
sed -n '500,730p' packages/client-runtime/src/state/runtime.ts
sed -n '1030,1125p' packages/client-runtime/src/connection/registry.tsRepository: pingdotgg/t3code Length of output: 13250 🏁 Script executed: set -o pipefail
rg -n -F --glob '*.ts' -e 'followStream' packages/client-runtime/src/connection/registry.ts
rg -n -F --glob '*.ts' -e 'runStream' packages/client-runtime/src/connection/registry.ts
sed -n '300,470p' packages/client-runtime/src/connection/registry.ts
sed -n '470,650p' packages/client-runtime/src/connection/registry.tsRepository: pingdotgg/t3code Length of output: 13911 🏁 Script executed: set -o pipefail
rg -n -F --glob '*.ts' -e 'export function subscribe' -e 'function subscribe' -e 'const subscribe' packages/client-runtime/src/rpc/client.ts
sed -n '1,180p' packages/client-runtime/src/rpc/client.ts
sed -n '180,300p' packages/client-runtime/src/rpc/client.tsRepository: pingdotgg/t3code Length of output: 11824 🏁 Script executed: set -o pipefail
sed -n '260,455p' packages/client-runtime/src/rpc/client.tsRepository: pingdotgg/t3code Length of output: 7636 Make the epoch transition session-aware before seeding
Do not accept every mismatched epoch. Preview uses the untagged 🤖 Prompt for AI Agents |
||
| }); | ||
| get.mount(events); | ||
| }); | ||
| }); | ||
|
|
||
| const noServerTabOpeningsAtom = Atom.make(() => undefined); | ||
|
|
||
| export function ElectronBrowserHost() { | ||
| const previewByThreadKey = useActivePreviewSessions(); | ||
| const primaryEnvironmentId = useAtomValue(primaryEnvironmentIdAtom); | ||
| useAtomValue( | ||
| isElectron && primaryEnvironmentId !== null | ||
| ? serverTabOpeningsAtom(primaryEnvironmentId) | ||
| : noServerTabOpeningsAtom, | ||
| ); | ||
| // A seeded tab's thread stays synced so its navigation and closing reach the webview below. | ||
| const syncedThreadRefs = Object.keys(previewByThreadKey).flatMap((threadKey) => { | ||
| const threadRef = parseScopedThreadKey(threadKey); | ||
| return threadRef?.environmentId === primaryEnvironmentId ? [threadRef] : []; | ||
| }); | ||
| const activeSessions = Object.entries(previewByThreadKey).flatMap(([threadKey, previewState]) => { | ||
| const threadRef = parseScopedThreadKey(threadKey); | ||
| return threadRef | ||
|
|
@@ -116,6 +156,9 @@ export function ElectronBrowserHost() { | |
| if (!isElectron) return null; | ||
| return ( | ||
| <div className="contents" data-electron-browser-host> | ||
| {syncedThreadRefs.map((threadRef) => ( | ||
| <PreviewSessionSync key={scopedThreadKey(threadRef)} threadRef={threadRef} /> | ||
| ))} | ||
| {sessions.map(({ threadRef, snapshot, runtimeTabId, pictureInPicture, zoomFactor }) => { | ||
| const url = snapshot.navStatus._tag === "Idle" ? null : snapshot.navStatus.url; | ||
| return ( | ||
|
|
@@ -145,6 +188,11 @@ export function ElectronBrowserHost() { | |
| ); | ||
| } | ||
|
|
||
| function PreviewSessionSync({ threadRef }: { readonly threadRef: ScopedThreadRef }) { | ||
| usePreviewSession(threadRef); | ||
| return null; | ||
| } | ||
|
|
||
| function AuthorizedBrowserWebview(props: ComponentProps<typeof HostedBrowserWebview>) { | ||
| const canOperatePreview = useEnvironmentScope( | ||
| props.threadRef.environmentId, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
browser/ElectronBrowserHost.tsx:51After a server restart, this subscriber drops every
openedevent for a previously used thread, so its new agent tabs are not seeded into the desktop browser profile and fall back to the server's unsigned-in browser. The thread's keep-alive state still has the oldserverEpoch, soapplyPreviewServerEventrejects the event with the new epoch; reconcile the thread list on epoch changes, asusePreviewSessiondoes.🤖 Copy this AI Prompt to have your agent fix this: