From 8be19ed3f1f22837edb94dfd9324555fae042c24 Mon Sep 17 00:00:00 2001 From: astarktc <148526207+astarktc@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:27:13 -0600 Subject: [PATCH 001/167] fix(pi): short-lived Pi processes no longer lose rotated OAuth tokens (#30187) --- .../provider-pi/src/server/adapter.test.ts | 4 +++ packages/provider-pi/src/server/adapter.ts | 1 + .../provider-pi/src/server/driver.test.ts | 11 +++++++ .../src/server/mcpInjection.test.ts | 11 +++++++ .../provider-pi/src/server/mcpInjection.ts | 4 +++ packages/provider-pi/src/server/status.ts | 1 + .../src/server/textGeneration.test.ts | 30 +++++++++++++++++++ .../provider-pi/src/server/textGeneration.ts | 1 + 8 files changed, 63 insertions(+) diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index 0afd6960485de..dc994f90cac5a 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -1591,6 +1591,9 @@ describe("PiAdapterV2", () => { arg === "--extension" ? [spawn.args[index + 1]] : [], ); assert.isFalse(spawn.args.includes("--no-extensions")); + // A thread's long-lived process keeps Pi's startup model refresh unless the user opts out. + assert.notInclude(spawn.args, "--offline"); + assert.isUndefined(spawn.env.PI_OFFLINE); assert.isTrue(extensions.some((path) => path?.endsWith("pi-t3-mcp-extension.ts"))); assert.equal(spawn.env.T3_MCP_URL, "http://127.0.0.1:43123/mcp"); assert.equal(spawn.env.T3_MCP_BEARER_TOKEN, "secret-pi-token"); @@ -1891,6 +1894,7 @@ describe("PiAdapterV2", () => { assert.equal(args[args.indexOf("--fork") + 1], FAKE_SESSION_FILE); assert.include(args, "--no-extensions"); assert.include(args, "--no-tools"); + assert.include(args, "--offline"); assert.notInclude(args, "--no-session"); assert.deepEqual( forkFake diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index b7045bcd64360..a8e140f02b784 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3103,6 +3103,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* ( environment: options.environment, mcpSession: undefined, extensionPath: undefined, + shortLived: true, disableExtensions: true, disableTools: true, }); diff --git a/packages/provider-pi/src/server/driver.test.ts b/packages/provider-pi/src/server/driver.test.ts index 087a8089aaee8..59586fdaedb39 100644 --- a/packages/provider-pi/src/server/driver.test.ts +++ b/packages/provider-pi/src/server/driver.test.ts @@ -169,6 +169,17 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => { ["--approve", "--skill", "extra skill", "--no-session"], ); assert.equal(workspaceLaunch!.options.env?.PI_CODING_AGENT_DIR, "/isolated-pi"); + // Discovery processes are stopped as soon as they answer, so neither may start Pi's + // network refresh at launch. + for (const cwd of ["/machine", "/first"]) { + const discovery = launches.find( + (launch) => launch.options.cwd === cwd && launch.args.includes("--mode"), + ); + assert.deepEqual( + discovery?.args.filter((arg) => arg === "--offline"), + ["--offline"], + ); + } }).pipe(Effect.scoped), ); diff --git a/packages/provider-pi/src/server/mcpInjection.test.ts b/packages/provider-pi/src/server/mcpInjection.test.ts index 2ca5f35d026f5..4e776058a66a4 100644 --- a/packages/provider-pi/src/server/mcpInjection.test.ts +++ b/packages/provider-pi/src/server/mcpInjection.test.ts @@ -102,6 +102,7 @@ describe("pi T3 MCP injection", () => { extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts", sessionPath: "/fake/persisted.jsonl", ephemeral: true, + shortLived: true, disableExtensions: true, disableTools: true, }); @@ -111,9 +112,19 @@ describe("pi T3 MCP injection", () => { "--no-session", "--provider", "anthropic", + "--offline", "--no-extensions", "--no-tools", ]); + const alreadyOffline = buildPiRpcLaunch({ + launchArgs: ["--offline"], + environment: {}, + mcpSession: undefined, + extensionPath: undefined, + ephemeral: true, + shortLived: true, + }); + assert.deepEqual(alreadyOffline.args, ["--mode", "rpc", "--no-session", "--offline"]); assert.deepInclude(resolvePiLaunchArgs("--mode text"), { ok: false, message: "Pi launch argument '--mode' is controlled by T3 Code and cannot be overridden.", diff --git a/packages/provider-pi/src/server/mcpInjection.ts b/packages/provider-pi/src/server/mcpInjection.ts index 51848c424ca05..ab92ab73dde7a 100644 --- a/packages/provider-pi/src/server/mcpInjection.ts +++ b/packages/provider-pi/src/server/mcpInjection.ts @@ -244,6 +244,9 @@ export function buildPiRpcLaunch(input: { readonly extensionPath: string | undefined; readonly sessionPath?: string | undefined; readonly ephemeral?: boolean; + // T3 stops these processes as soon as they answer. They run --offline because Pi's startup + // catalog refresh renews expired OAuth tokens, and a stop mid-request loses the rotated one. + readonly shortLived?: boolean; readonly disableExtensions?: boolean; readonly disableTools?: boolean; readonly runtimeMode?: "approval-required" | "auto-accept-edits" | "auto" | "full-access"; @@ -268,6 +271,7 @@ export function buildPiRpcLaunch(input: { ? ["--session", input.sessionPath] : []), ...launchArgs, + ...(input.shortLived === true && !launchArgs.includes("--offline") ? ["--offline"] : []), // Restrictions must follow launch args so user flags cannot re-enable unattended tools/extensions. ...(input.disableExtensions === true ? ["--no-extensions"] : []), ...(input.disableTools === true ? ["--no-tools"] : []), diff --git a/packages/provider-pi/src/server/status.ts b/packages/provider-pi/src/server/status.ts index dfafa75ccd579..4775689c8c2b7 100644 --- a/packages/provider-pi/src/server/status.ts +++ b/packages/provider-pi/src/server/status.ts @@ -150,6 +150,7 @@ const makePiDiscoveryConnection = Effect.fnUntraced(function* ( mcpSession: undefined, extensionPath: undefined, ephemeral: true, + shortLived: true, }); const connection = yield* makePiRpcConnection({ command: piSettings.binaryPath || "pi", diff --git a/packages/provider-pi/src/server/textGeneration.test.ts b/packages/provider-pi/src/server/textGeneration.test.ts index ba693da18012b..0e6a8a3c07780 100644 --- a/packages/provider-pi/src/server/textGeneration.test.ts +++ b/packages/provider-pi/src/server/textGeneration.test.ts @@ -7,6 +7,7 @@ import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { makePiTextGeneration } from "./textGeneration.ts"; @@ -68,6 +69,35 @@ const makeFakePi = ( return { spawner, received }; }); +it.effect("runs Pi offline, since the process is stopped as soon as it answers", () => + Effect.gen(function* () { + const pi = yield* makeFakePi(JSON.stringify({ subject: "Update README", body: "" })); + const launches: Array> = []; + const spawner = ChildProcessSpawner.make((command) => { + if (ChildProcess.isStandardCommand(command)) launches.push(command.args.map(String)); + return pi.spawner.spawn(command); + }); + const textGeneration = yield* makePiTextGeneration( + { enabled: true, binaryPath: "pi", launchArgs: "", customModels: [] }, + {}, + ).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)); + + const result = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "main", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md b/README.md", + modelSelection: createModelSelection(ProviderInstanceId.make("pi"), "default"), + }); + + assert.equal(result.subject, "Update README"); + assert.deepEqual( + launches.map((args) => args.filter((arg) => arg === "--offline")), + [["--offline"]], + ); + }), +); + it.effect("names the model and the disabled extensions when Pi cannot load it", () => Effect.gen(function* () { const pi = yield* makeFakePi("", "Model not found: demo/demo-model"); diff --git a/packages/provider-pi/src/server/textGeneration.ts b/packages/provider-pi/src/server/textGeneration.ts index 3fed9963b6cea..40697f2d4280a 100644 --- a/packages/provider-pi/src/server/textGeneration.ts +++ b/packages/provider-pi/src/server/textGeneration.ts @@ -47,6 +47,7 @@ export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* mcpSession: undefined, extensionPath: undefined, ephemeral: true, + shortLived: true, disableExtensions: true, // Background naming/content helpers must never mutate the workspace. disableTools: true, From e27e12d9b4a408496be359e3e0db1844c6ea87af Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:57:58 +0530 Subject: [PATCH 002/167] feat(web): add refresh to provider status banners (#24861) --- apps/web/src/components/ChatView.tsx | 1 + .../src/components/chat/ChatBannerLane.tsx | 3 + .../components/chat/ProviderStatusBanner.tsx | 69 +++++++++++++++---- .../src/state/commandPermissions.test.ts | 34 +++++++++ .../contracts/src/clientRpcPermissions.ts | 1 + 5 files changed, 95 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 6ec89f8c36256..497ccfc877771 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3367,6 +3367,7 @@ export default function ChatView(props: ChatViewProps) { {isWorkspaceFileDragActive ? : null} >; @@ -41,6 +43,7 @@ export function ChatBannerLane(props: { return (
setDismissedProviderStatusBannerKey(providerStatusBannerKey)} onOpenProviderSetup={onOpenProviderSetup} diff --git a/apps/web/src/components/chat/ProviderStatusBanner.tsx b/apps/web/src/components/chat/ProviderStatusBanner.tsx index 0e258c10906ac..64afa55ed0d78 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.tsx @@ -1,10 +1,18 @@ -import { type ProviderInstanceId, type ServerProvider } from "@t3tools/contracts"; -import { memo } from "react"; +import { + type EnvironmentId, + type ProviderInstanceId, + type ServerProvider, +} from "@t3tools/contracts"; +import { useAtomValue } from "@effect/atom-react"; +import { memo, useRef, useState } from "react"; import { InfoIcon, XIcon } from "lucide-react"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert"; import { Button, InlineButton } from "../ui/button"; import { formatProviderDriverKindLabel } from "../../providerModels"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { serverEnvironment } from "../../state/server"; +import { useAtomCommand } from "../../state/use-atom-command"; +import { RefreshIcon } from "../ui/refresh-icon"; function getIncompatibleVersion(status: ServerProvider) { const compatibility = status.compatibilityAdvisory; @@ -86,14 +94,35 @@ export function getProviderStatusMessage(status: ServerProvider): string { } export const ProviderStatusBanner = memo(function ProviderStatusBanner({ + environmentId, onDismiss, onOpenProviderSetup, status, }: { + environmentId: EnvironmentId; onDismiss: () => void; onOpenProviderSetup?: (instanceId: ProviderInstanceId) => void; status: ServerProvider | null; }) { + const canRefresh = useAtomValue(serverEnvironment.refreshProviders.permissionAtom(environmentId)); + const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { + label: "Refresh provider status", + }); + const refreshingRef = useRef(false); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refresh = async () => { + if (!status || !canRefresh || refreshingRef.current) return; + refreshingRef.current = true; + setIsRefreshing(true); + await refreshProviders({ + environmentId, + input: { instanceId: status.instanceId, refreshModels: true }, + }); + refreshingRef.current = false; + setIsRefreshing(false); + }; + if (!status || getProviderStatusBannerKey(status) === null) { return null; } @@ -121,17 +150,31 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({ {title} - - }>{message} - - {message} - - - {onOpenProviderSetup && hasProviderSetup(status) ? ( - onOpenProviderSetup(status.instanceId)}> - Open provider setup - - ) : null} +
+ + }>{message} + + {message} + + +
+ {onOpenProviderSetup && hasProviderSetup(status) ? ( + onOpenProviderSetup(status.instanceId)}> + Open provider setup + + ) : null} + +
+
+ {props.isSelected ?