diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 484c3fb78f9de..5f68e9a42a6f2 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -26,13 +26,8 @@ import { type ModelSelection, type ProviderInteractionMode, type ServerConfig as T3ServerConfig, - type UsageLimitsReport, } from "@t3tools/contracts"; -import { - collectProviderUsageLimits, - hasProviderUsageLimits, - isUsageLimitsCommand, -} from "@t3tools/shared/usageLimits"; +import { hasProviderUsageLimits, isUsageLimitsCommand } from "@t3tools/shared/usageLimits"; import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import type { ReactNode } from "react"; import { useEffect, useEffectEvent, useRef, useState, type RefObject } from "react"; @@ -258,7 +253,8 @@ export interface ThreadComposerProps { followUp?: ActiveTurnComposerAction, stillSendable?: () => boolean, ) => Promise; - readonly onShowUsageLimits: (report: UsageLimitsReport | null) => void; + /** Opens the limits panel; false when the provider reports none. */ + readonly onShowUsageLimits: () => boolean; readonly canSwitchProvider: boolean; readonly onUpdateInteractionMode?: (interactionMode: ProviderInteractionMode) => void; readonly onHeightChange?: (height: number) => void; @@ -533,19 +529,11 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps props.serverConfig?.usageLimitSources ?? [], ); const openUsageLimits = () => { - const report = collectProviderUsageLimits( - props.selectedThread.runtime?.providerInstanceId ?? - props.selectedThread.modelSelection.instanceId, - props.serverConfig?.providers ?? [], - props.serverConfig?.usageLimitSources ?? [], - // oxlint-disable-next-line react/purity -- The submit and menu handlers read usage limits only after a user action. - Date.now(), - ); - onShowUsageLimits(report); - if (!report) { + const shown = onShowUsageLimits(); + if (!shown) { Dialog.alert("Usage limits unavailable", "This provider does not currently report limits."); } - return report !== null; + return shown; }; const lockedProviderInstanceId = props.canSwitchProvider diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index bb20e8fda38b7..6774c0f241428 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -53,7 +53,6 @@ import type { RuntimeRequestId, ScopedThreadRef, ServerConfig as T3ServerConfig, - UsageLimitsReport, RunId, } from "@t3tools/contracts"; import { @@ -1025,9 +1024,12 @@ function ThreadDetailBody( readonly now: number; } | null>(null); // Approval/question identity invalidates quota: answering on any client resumes spending. + const usageLimitsInstanceId = + props.selectedThread.runtime?.providerInstanceId ?? + props.selectedThread.modelSelection.instanceId; const usageLimitsKey = [ selectedThreadKey, - props.selectedThread.modelSelection.instanceId, + usageLimitsInstanceId, props.selectedThread.latestRun?.runId ?? "", props.activePendingApproval?.requestId ?? props.activePendingUserInput?.requestId ?? "", ].join(":"); @@ -1037,22 +1039,25 @@ function ThreadDetailBody( const usageLimitsReport = usageLimitsPanel !== null && usageLimitsPanel.key === usageLimitsKey ? collectProviderUsageLimits( - props.selectedThread.modelSelection.instanceId, + usageLimitsInstanceId, props.serverConfig?.providers ?? [], props.serverConfig?.usageLimitSources ?? [], usageLimitsPanel.now, ) : null; - const showUsageLimits = (report: UsageLimitsReport | null) => + const showUsageLimits = () => { + const now = Date.now(); + const report = collectProviderUsageLimits( + usageLimitsInstanceId, + props.serverConfig?.providers ?? [], + props.serverConfig?.usageLimitSources ?? [], + now, + ); setUsageLimitsPanel( - report === null - ? null - : { - key: usageLimitsKey, - threadKey: selectedThreadKey, - now: Date.parse(report.createdAt), - }, + report === null ? null : { key: usageLimitsKey, threadKey: selectedThreadKey, now }, ); + return report !== null; + }; const dismissUsageLimits = () => setUsageLimitsPanel(null); // Late sends clear only their originating thread's limits panel. const clearUsageLimitsFor = (threadKey: string) => diff --git a/apps/server/src/peer/PeerMcpClient.ts b/apps/server/src/peer/PeerMcpClient.ts index d00c398b91dc6..b973584ddbf22 100644 --- a/apps/server/src/peer/PeerMcpClient.ts +++ b/apps/server/src/peer/PeerMcpClient.ts @@ -309,12 +309,11 @@ const make = Effect.gen(function* () { }), ); } + const success = input.success; const decoded = body.result.structuredContent === undefined - ? // oxlint-disable-next-line t3code/no-inline-schema-compile -- Each caller supplies its tool's result schema. - Schema.decodeEffect(Schema.fromJsonString(input.success))(text) - : // oxlint-disable-next-line t3code/no-inline-schema-compile -- Each caller supplies its tool's result schema. - Schema.decodeEffect(input.success)(body.result.structuredContent); + ? Schema.decodeEffect(Schema.fromJsonString(success))(text) + : Schema.decodeEffect(success)(body.result.structuredContent); return yield* decoded.pipe( Effect.map(Result.succeed), Effect.mapError( diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index efdb76aa97398..b50a47eb1896b 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -1,6 +1,8 @@ import "vite-plus/test/config"; // @effect-diagnostics-next-line nodeBuiltinImport:off -- tsdown needs the asset path when it reads this config. import * as NodePath from "node:path"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Effect from "effect/Effect"; import { defineConfig, mergeConfig } from "vite-plus"; import baseConfig from "../../vite.config.ts"; @@ -46,10 +48,9 @@ const packExecutableTarget = resolveCliExecutableTarget({ target: process.env.T3CODE_PACK_EXE_TARGET, nodeVersion: rootPackageJson.engines.node, hostNodeVersion: process.versions.node, - // oxlint-disable-next-line t3code/no-global-process-runtime -- Build config selects the running Node binary, including its emulated architecture, before the Effect runtime exists. - hostPlatform: process.platform, - // oxlint-disable-next-line t3code/no-global-process-runtime -- Build config must match the Node binary's architecture rather than the machine's physical architecture. - hostArch: process.arch, + // The running Node is the one tsdown embeds for a native build. + hostPlatform: Effect.runSync(HostProcess.Platform), + hostArch: Effect.runSync(HostProcess.Architecture), }); // The root config's `projects` route repo-root runs to package configs, diff --git a/packages/client-runtime/src/state/providerAccountSync.ts b/packages/client-runtime/src/state/providerAccountSync.ts index d572aea7e3502..bbc13e8460ba5 100644 --- a/packages/client-runtime/src/state/providerAccountSync.ts +++ b/packages/client-runtime/src/state/providerAccountSync.ts @@ -25,11 +25,15 @@ import { EnvironmentRpcUnavailableError, getInitialServerConfig, requestGuarded, - RpcPermissionGuard, } from "../rpc/client.ts"; import { createCommandPermissions } from "./commandPermissions.ts"; import { resolveProviderInstanceDisplayName } from "./providerInstanceDisplay.ts"; -import { type AtomCommand, createAtomCommandScheduler, createRuntimeCommand } from "./runtime.ts"; +import { + type AtomCommand, + createAtomCommandScheduler, + createRuntimeCommand, + withEnvironmentRpcPermissions, +} from "./runtime.ts"; export interface ProviderAccountSyncRequest { readonly sourceEnvironmentId: EnvironmentId; @@ -242,16 +246,7 @@ export function createProviderAccountSyncCommand( ]), }, execute: (request, atomRegistry) => - syncAccounts(request).pipe( - Effect.provideService(RpcPermissionGuard, { - authorize: (environmentId, method, payload) => - createCommandPermissions(runtime, method).authorize( - atomRegistry, - environmentId, - payload, - ), - }), - ), + syncAccounts(request).pipe(withEnvironmentRpcPermissions(runtime, atomRegistry)), }); return { ...command, diff --git a/vite.config.ts b/vite.config.ts index 91de14b1961f5..828b6a0a23b7d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -356,7 +356,6 @@ export default defineConfig({ files: [ "packages/client-runtime/src/state/runtime.ts", "packages/client-runtime/src/state/vcsAction.ts", - "packages/client-runtime/src/state/providerAccountSync.ts", ], rules: { "t3code/no-rpc-permission-bypass": [