History

Merge upstream/main: Effect-service refactors

Resolve conflicts from upstream's Effect-service normalization batch while preserving the WSL parallel-backend feature set. Key resolutions: - ipc/methods/window.ts: adopt the DesktopIpc.makeIpcMethod / DesktopIpc.makeSyncIpcMethod namespace convention; keep our plural getLocalEnvironmentBootstraps and the getLocalEnvironmentBearerToken method. - app/DesktopState.ts: adopt upstream's Context.Service class with backendReady + quitting. The window keeps its own backendReadyRef, so state.backendReady is unused but harmless. - app/DesktopObservability.ts: keep our per-instance DesktopBackendOutputLogFactory (per-instance backend logs are required for parallel backends). Drop upstream's newly-extracted single-instance DesktopBackendOutputLog.ts: zero consumers in this tree, fully superseded by the factory (primary maps to forInstance(primary), same server-child.log path). - backend/DesktopBackendManager.test.ts: keep our factory-based harness. Upstream's side provisioned the removed DesktopBackendManager.layer service model (DesktopState / DesktopWindow / DesktopBackendConfiguration deps), which no longer exists in this tree. - Adapt DesktopObservability.ts + ipc/methods/wsl.test.ts to upstream's removal of the standalone DesktopEnvironmentShape export: use DesktopEnvironment.DesktopEnvironment["Service"] (the normalized form).

Josh Gratton committed 4 months ago

Commit 8e3012d2 parents 2a3cdd5 9544e72181 files · +3745 −2756Browse files

Unified · Split

181 files
  1. .github/workflows/mobile-eas-preview.yml +2 −0
  2. apps/desktop/src/app/DesktopAppIdentity.test.ts +2 −2
  3. apps/desktop/src/app/DesktopAppIdentity.ts +4 −6
  4. apps/desktop/src/app/DesktopAssets.ts +7 −8
  5. apps/desktop/src/app/DesktopClerk.test.ts +8 −9
  6. apps/desktop/src/app/DesktopClerk.ts +10 −11
  7. apps/desktop/src/app/DesktopEnvironment.ts +50 −55
  8. apps/desktop/src/app/DesktopLifecycle.ts +89 −120
  9. apps/desktop/src/app/DesktopObservability.ts +3 −3
  10. apps/desktop/src/app/DesktopShutdown.ts +35 −0
  11. apps/desktop/src/app/DesktopState.ts +12 −12
  12. apps/desktop/src/ipc/DesktopIpc.ts +16 −14
  13. apps/desktop/src/ipc/methods/clientSettings.ts +3 −3
  14. apps/desktop/src/ipc/methods/connectionCatalog.ts +4 −4
  15. apps/desktop/src/ipc/methods/preview.ts +23 −23
  16. apps/desktop/src/ipc/methods/serverExposure.ts +5 −5
  17. apps/desktop/src/ipc/methods/sshEnvironment.ts +9 −9
  18. apps/desktop/src/ipc/methods/updates.ts +6 −6
  19. apps/desktop/src/ipc/methods/window.ts +9 −9
  20. apps/desktop/src/ipc/methods/wsl.test.ts +1 −1
  21. apps/desktop/src/main.ts +3 −4
  22. apps/desktop/src/preview/Manager.test.ts +2 −2
  23. apps/desktop/src/shell/DesktopShellEnvironment.test.ts +1 −1
  24. apps/desktop/src/updates/DesktopUpdates.ts +1 −1
  25. apps/desktop/src/window/DesktopWindow.test.ts +6 −6
  26. apps/mobile/.swiftlint.yml +1 −0
  27. apps/mobile/global.css +21 −1
  28. apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +27 −4
  29. apps/mobile/src/app/+not-found.tsx +2 −2
  30. apps/mobile/src/app/connections/index.tsx +1 −1
  31. apps/mobile/src/app/connections/new.tsx +5 −5
  32. apps/mobile/src/app/new/index.tsx +5 −7
  33. apps/mobile/src/app/settings/environments.tsx +11 −11
  34. apps/mobile/src/app/settings/index.tsx +7 −7
  35. apps/mobile/src/components/AppText.tsx +1 −1
  36. apps/mobile/src/components/BrandMark.tsx +3 −6
  37. apps/mobile/src/components/ComposerToolbarTrigger.tsx +1 −1
  38. apps/mobile/src/components/ControlPill.tsx +1 −1
  39. apps/mobile/src/components/EmptyState.tsx +1 −3
  40. apps/mobile/src/components/ErrorBanner.tsx +1 −1
  41. apps/mobile/src/components/StatusPill.tsx +1 −1
  42. apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +11 −9
  43. apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx +8 −12
  44. apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx +9 −12
  45. apps/mobile/src/features/connection/ConnectionSheetButton.tsx +1 −1
  46. apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx +3 −3
  47. apps/mobile/src/features/files/FileMarkdownPreview.tsx +3 −4
  48. apps/mobile/src/features/files/FileTreeBrowser.tsx +6 −8
  49. apps/mobile/src/features/files/SourceFileSurface.tsx +15 −9
  50. apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +8 −7
  51. apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx +2 −2
  52. apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx +3 −5
  53. apps/mobile/src/features/files/nativeSourceFileAdapter.test.ts +20 −0
  54. apps/mobile/src/features/files/nativeSourceFileAdapter.ts +10 −8
  55. apps/mobile/src/features/home/HomeHeader.tsx +3 −2
  56. apps/mobile/src/features/home/HomeScreen.tsx +7 −7
  57. apps/mobile/src/features/home/thread-swipe-actions.tsx +1 −1
  58. apps/mobile/src/features/projects/AddProjectScreen.tsx +11 −11
  59. apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +8 −8
  60. apps/mobile/src/features/review/ReviewSheet.tsx +19 −18
  61. apps/mobile/src/features/review/nativeReviewDiffAdapter.ts +7 −6
  62. apps/mobile/src/features/review/reviewDiffRendering.tsx +14 −5
  63. apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +6 −5
  64. apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx +3 −3
  65. apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +3 −2
  66. apps/mobile/src/features/threads/ComposerCommandPopover.tsx +13 −4
  67. apps/mobile/src/features/threads/GitActionProgressOverlay.tsx +2 −2
  68. apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +2 −1
  69. apps/mobile/src/features/threads/PendingApprovalCard.tsx +1 −1
  70. apps/mobile/src/features/threads/PendingUserInputCard.tsx +4 −4
  71. apps/mobile/src/features/threads/ThreadComposer.tsx +5 −6
  72. apps/mobile/src/features/threads/ThreadFeed.tsx +12 −14
  73. apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx +5 −7
  74. apps/mobile/src/features/threads/ThreadRouteScreen.tsx +3 −2
  75. apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +10 −12
  76. apps/mobile/src/features/threads/git/GitCommitSheet.tsx +18 −18
  77. apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +3 −3
  78. apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +3 −3
  79. apps/mobile/src/features/threads/git/gitSheetComponents.tsx +5 −5
  80. apps/mobile/src/features/threads/thread-work-log.tsx +3 −3
  81. apps/mobile/src/lib/typography.test.ts +23 −0
  82. apps/mobile/src/lib/typography.ts +22 −0
  83. apps/mobile/src/native/T3ComposerEditor.ios.tsx +9 −2
  84. apps/mobile/src/native/T3ComposerEditor.tsx +2 −2
  85. apps/server/src/auth/EnvironmentAuth.test.ts +10 −9
  86. apps/server/src/auth/EnvironmentAuthAdmin.test.ts +6 −7
  87. apps/server/src/auth/EnvironmentAuthPolicy.test.ts +7 −6
  88. apps/server/src/auth/PairingGrantStore.test.ts +6 −7
  89. apps/server/src/auth/SessionStore.test.ts +8 −9
  90. apps/server/src/bin.test.ts +11 −14
  91. apps/server/src/cli/auth.ts +3 −3
  92. apps/server/src/cli/config.ts +12 −20
  93. apps/server/src/cli/connect.ts +9 −9
  94. apps/server/src/cli/project.ts +21 −14
  95. apps/server/src/config.ts +83 −76
  96. apps/server/src/environment/Layers/ServerEnvironment.test.ts +8 −10
  97. apps/server/src/keybindings.test.ts +48 −55
  98. apps/server/src/keybindings.ts +63 −67
  99. apps/server/src/mcp/McpHttpServer.test.ts +9 −1
  100. apps/server/src/mcp/PreviewAutomationBroker.test.ts +122 −20
  101. apps/server/src/mcp/PreviewAutomationBroker.ts +40 −25
  102. apps/server/src/provider/Layers/ProviderRegistry.test.ts +116 −76
  103. apps/server/src/provider/providerUpdateSettings.ts +2 −2
  104. apps/server/src/server.test.ts +95 −100
  105. apps/server/src/server.ts +17 −17
  106. apps/server/src/serverLifecycleEvents.test.ts +3 −3
  107. apps/server/src/serverLifecycleEvents.ts +34 −37
  108. apps/server/src/serverRuntimeStartup.test.ts +32 −42
  109. apps/server/src/serverRuntimeStartup.ts +53 −48
  110. apps/server/src/serverRuntimeState.ts +3 −3
  111. apps/server/src/serverSettings.test.ts +19 −17
  112. apps/server/src/serverSettings.ts +56 −57
  113. apps/server/src/vcs/GitVcsDriverCore.test.ts +73 −0
  114. apps/server/src/vcs/GitVcsDriverCore.ts +25 −6
  115. apps/server/src/ws.ts +2 −2
  116. apps/web/src/components/ChatView.tsx +13 −120
  117. apps/web/src/components/DiffPanel.tsx +436 −345
  118. apps/web/src/components/DiffPanelShell.tsx +2 −8
  119. apps/web/src/components/diffs/AnnotatableFileDiff.tsx +209 −2
  120. apps/web/src/components/preview/PreviewAutomationOwner.test.ts +2 −2
  121. apps/web/src/components/preview/PreviewAutomationOwner.tsx +86 −99
  122. apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts +81 −0
  123. apps/web/src/components/preview/previewAutomationRequestConsumer.ts +83 −0
  124. apps/web/src/diffPanelStore.test.ts +68 −0
  125. apps/web/src/diffPanelStore.ts +139 −0
  126. apps/web/src/diffRouteSearch.test.ts +0 −74
  127. apps/web/src/diffRouteSearch.ts +0 −39
  128. apps/web/src/environments/primary/httpClient.ts +5 −8
  129. apps/web/src/index.css +2 −1
  130. apps/web/src/lib/baseRefChoices.test.ts +52 −0
  131. apps/web/src/lib/baseRefChoices.ts +61 −0
  132. apps/web/src/lib/diffRendering.test.ts +54 −1
  133. apps/web/src/lib/diffRendering.ts +41 −1
  134. apps/web/src/lib/runtime.ts +6 −8
  135. apps/web/src/rightPanelStore.test.ts +0 −11
  136. apps/web/src/rightPanelStore.ts +0 −10
  137. apps/web/src/routes/_chat.$environmentId.$threadId.tsx +1 −6
  138. infra/relay/alchemy.run.ts +3 −3
  139. infra/relay/src/Config.ts +19 −14
  140. infra/relay/src/agentActivity/AgentActivityPublisher.test.ts +2 −2
  141. infra/relay/src/agentActivity/AgentActivityRows.ts +2 −2
  142. infra/relay/src/agentActivity/ApnsClient.ts +6 −6
  143. infra/relay/src/agentActivity/ApnsDeliveries.test.ts +2 −2
  144. infra/relay/src/agentActivity/DeliveryAttempts.test.ts +13 −13
  145. infra/relay/src/agentActivity/DeliveryAttempts.ts +2 −2
  146. infra/relay/src/agentActivity/Devices.test.ts +13 −7
  147. infra/relay/src/agentActivity/Devices.ts +2 −2
  148. infra/relay/src/agentActivity/LiveActivities.test.ts +9 −5
  149. infra/relay/src/agentActivity/LiveActivities.ts +2 −2
  150. infra/relay/src/agentActivity/MobileRegistrations.test.ts +3 −3
  151. infra/relay/src/auth/DpopProofs.test.ts +9 −5
  152. infra/relay/src/auth/DpopProofs.ts +25 −28
  153. infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts +3 −3
  154. infra/relay/src/auth/RelayTokens.test.ts +1 −3
  155. infra/relay/src/auth/RelayTokens.ts +36 −37
  156. infra/relay/src/db.ts +6 −5
  157. infra/relay/src/environments/EnvironmentConnector.test.ts +5 −5
  158. infra/relay/src/environments/EnvironmentConnector.ts +15 −15
  159. infra/relay/src/environments/EnvironmentCredentials.test.ts +5 −5
  160. infra/relay/src/environments/EnvironmentCredentials.ts +18 −20
  161. infra/relay/src/environments/EnvironmentLinker.test.ts +3 −3
  162. infra/relay/src/environments/EnvironmentLinker.ts +19 −20
  163. infra/relay/src/environments/EnvironmentLinks.test.ts +16 −8
  164. infra/relay/src/environments/EnvironmentLinks.ts +40 −41
  165. infra/relay/src/environments/EnvironmentPublishSignatures.test.ts +2 −2
  166. infra/relay/src/environments/EnvironmentPublishSignatures.ts +8 −10
  167. infra/relay/src/environments/ManagedEndpointAllocations.ts +26 −28
  168. infra/relay/src/environments/ManagedEndpointProvider.test.ts +3 −3
  169. infra/relay/src/environments/ManagedEndpointProvider.ts +69 −63
  170. infra/relay/src/http/Api.test.ts +2 −2
  171. infra/relay/src/http/Api.ts +8 −5
  172. infra/relay/src/observability.test.ts +2 −2
  173. infra/relay/src/worker.ts +4 −9
  174. packages/client-runtime/src/state/preview.ts +4 −0
  175. packages/contracts/src/git.ts +2 −0
  176. packages/contracts/src/ipc.ts +2 −1
  177. packages/contracts/src/previewAutomation.ts +6 −1
  178. packages/contracts/src/relay.ts +11 −15
  179. packages/contracts/src/review.ts +1 −0
  180. packages/contracts/src/rpc.ts +3 −2
  181. pnpm-lock.yaml +250 −252
.github/workflows/mobile-eas-preview.yml+2−0
@@ -2,10 +2,12 @@ name: Mobile EAS Preview · 1 unmodified line
2 2
3on:3on:
4 pull_request:4 pull_request:
5 types: [opened, reopened, synchronize, labeled, unlabeled]
5 6
6jobs:7jobs:
7 preview:8 preview:
8 name: EAS Preview9 name: EAS Preview
10 if: contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment')
9 runs-on: blacksmith-8vcpu-ubuntu-240411 runs-on: blacksmith-8vcpu-ubuntu-2404
10 permissions:12 permissions:
11 contents: read13 contents: read
apps/desktop/src/app/DesktopAppIdentity.test.ts+2−2
@@ -63,7 +63,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) => · 62 unmodified lines
63 }),63 }),
64 appendCommandLineSwitch: () => Effect.void,64 appendCommandLineSwitch: () => Effect.void,
65 on: () => Effect.void,65 on: () => Effect.void,
66 } satisfies ElectronApp.ElectronAppShape);66 } satisfies ElectronApp.ElectronApp["Service"]);
67 67
68const makeAssetsLayer = (png: Option.Option<string>) =>68const makeAssetsLayer = (png: Option.Option<string>) =>
69 Layer.succeed(DesktopAssets.DesktopAssets, {69 Layer.succeed(DesktopAssets.DesktopAssets, {
@@ -73,7 +73,7 @@ const makeAssetsLayer = (png: Option.Option<string>) => · 3 unmodified lines
73 png,73 png,
74 }),74 }),
75 resolveResourcePath: () => Effect.succeed(Option.none()),75 resolveResourcePath: () => Effect.succeed(Option.none()),
76 } satisfies DesktopAssets.DesktopAssetsShape);76 } satisfies DesktopAssets.DesktopAssets["Service"]);
77 77
78const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => {78const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => {
79 const { env, ...environmentOverrides } = overrides;79 const { env, ...environmentOverrides } = overrides;
apps/desktop/src/app/DesktopAppIdentity.ts+4−6
@@ -18,14 +18,12 @@ const AppPackageMetadata = Schema.Struct({ · 17 unmodified lines
18});18});
19const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));19const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));
20 20
21export interface DesktopAppIdentityShape {
22 readonly resolveUserDataPath: Effect.Effect<string>;
23 readonly configure: Effect.Effect<void>;
24}
25
26export class DesktopAppIdentity extends Context.Service<21export class DesktopAppIdentity extends Context.Service<
27 DesktopAppIdentity,22 DesktopAppIdentity,
28 DesktopAppIdentityShape23 {
24 readonly resolveUserDataPath: Effect.Effect<string>;
25 readonly configure: Effect.Effect<void>;
26 }
29>()("@t3tools/desktop/app/DesktopAppIdentity") {}27>()("@t3tools/desktop/app/DesktopAppIdentity") {}
30 28
31const normalizeCommitHash = (value: string): Option.Option<string> => {29const normalizeCommitHash = (value: string): Option.Option<string> => {
apps/desktop/src/app/DesktopAssets.ts+7−8
@@ -12,14 +12,13 @@ export interface DesktopIconPaths { · 11 unmodified lines
12 readonly png: Option.Option<string>;12 readonly png: Option.Option<string>;
13}13}
14 14
15export interface DesktopAssetsShape {15export class DesktopAssets extends Context.Service<
16 readonly iconPaths: Effect.Effect<DesktopIconPaths>;16 DesktopAssets,
17 readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;17 {
18}18 readonly iconPaths: Effect.Effect<DesktopIconPaths>;
19 19 readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;
20export class DesktopAssets extends Context.Service<DesktopAssets, DesktopAssetsShape>()(20 }
21 "@t3tools/desktop/app/DesktopAssets",21>()("@t3tools/desktop/app/DesktopAssets") {}
22) {}
23 22
24const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(function* (23const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(function* (
25 fileName: string,24 fileName: string,
apps/desktop/src/app/DesktopClerk.test.ts+8−9
@@ -21,10 +21,6 @@ vi.mock("@clerk/electron/storage", () => ({ · 20 unmodified lines
21 storage: storageMock,21 storage: storageMock,
22}));22}));
23 23
24import {
25 createDesktopClerkBridge,
26 resolveDesktopClerkFrontendApiHostname,
27} from "./DesktopClerk.ts";
28import * as DesktopClerk from "./DesktopClerk.ts";24import * as DesktopClerk from "./DesktopClerk.ts";
29import * as DesktopEnvironment from "./DesktopEnvironment.ts";25import * as DesktopEnvironment from "./DesktopEnvironment.ts";
30 26
@@ -32,9 +28,12 @@ describe("DesktopClerk", () => { · 1 unmodified line
32 it("derives the Clerk Frontend API hostname used by the desktop CSP", () => {28 it("derives the Clerk Frontend API hostname used by the desktop CSP", () => {
33 const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`;29 const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`;
34 30
35 assert.equal(resolveDesktopClerkFrontendApiHostname(publishableKey), "clerk.t3.codes");31 assert.equal(
36 assert.equal(resolveDesktopClerkFrontendApiHostname(""), undefined);32 DesktopClerk.resolveDesktopClerkFrontendApiHostname(publishableKey),
37 assert.equal(resolveDesktopClerkFrontendApiHostname("invalid"), undefined);33 "clerk.t3.codes",
34 );
35 assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname(""), undefined);
36 assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined);
38 });37 });
39 38
40 it.effect("acquires and releases the SDK bridge with the layer", () => {39 it.effect("acquires and releases the SDK bridge with the layer", () => {
@@ -44,7 +43,7 @@ describe("DesktopClerk", () => { · 3 unmodified lines
44 const environment = DesktopEnvironment.DesktopEnvironment.of({43 const environment = DesktopEnvironment.DesktopEnvironment.of({
45 stateDir: "/tmp/t3-state",44 stateDir: "/tmp/t3-state",
46 isDevelopment: true,45 isDevelopment: true,
47 } as unknown as DesktopEnvironment.DesktopEnvironmentShape);46 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
48 47
49 return Effect.gen(function* () {48 return Effect.gen(function* () {
50 yield* Effect.scoped(49 yield* Effect.scoped(
@@ -78,7 +77,7 @@ describe("DesktopClerk", () => { · 27 unmodified lines
78 storageMock.mockReturnValue(storageAdapter);77 storageMock.mockReturnValue(storageAdapter);
79 createClerkBridgeMock.mockReturnValue(bridge);78 createClerkBridgeMock.mockReturnValue(bridge);
80 79
81 assert.equal(createDesktopClerkBridge("/tmp/t3-state", isDevelopment), bridge);80 assert.equal(DesktopClerk.createDesktopClerkBridge("/tmp/t3-state", isDevelopment), bridge);
82 assert.deepEqual(storageMock.mock.calls, [[{ path: "/tmp/t3-state" }]]);81 assert.deepEqual(storageMock.mock.calls, [[{ path: "/tmp/t3-state" }]]);
83 assert.deepEqual(createClerkBridgeMock.mock.calls, [82 assert.deepEqual(createClerkBridgeMock.mock.calls, [
84 [83 [
apps/desktop/src/app/DesktopClerk.ts+10−11
@@ -14,17 +14,16 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts"; · 13 unmodified lines
14 14
15declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;15declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;
16 16
17export interface DesktopClerkShape {17export class DesktopClerk extends Context.Service<
18 readonly configure: Effect.Effect<18 DesktopClerk,
19 void,19 {
20 never,20 readonly configure: Effect.Effect<
21 ElectronApp.ElectronApp | ElectronWindow.ElectronWindow | Scope.Scope21 void,
22 >;22 never,
23}23 ElectronApp.ElectronApp | ElectronWindow.ElectronWindow | Scope.Scope
24 24 >;
25export class DesktopClerk extends Context.Service<DesktopClerk, DesktopClerkShape>()(25 }
26 "@t3tools/desktop/app/DesktopClerk",26>()("@t3tools/desktop/app/DesktopClerk") {}
27) {}
28 27
29export function resolveDesktopClerkFrontendApiHostname(28export function resolveDesktopClerkFrontendApiHostname(
30 publishableKey: string | undefined,29 publishableKey: string | undefined,
apps/desktop/src/app/DesktopEnvironment.ts+50−55
@@ -11,10 +11,7 @@ import * as Layer from "effect/Layer"; · 10 unmodified lines
11import * as Option from "effect/Option";11import * as Option from "effect/Option";
12import * as Path from "effect/Path";12import * as Path from "effect/Path";
13 13
14import {14import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
15 type DesktopSettings,
16 resolveDefaultDesktopSettings,
17} from "../settings/DesktopAppSettings.ts";
18import * as DesktopConfig from "./DesktopConfig.ts";15import * as DesktopConfig from "./DesktopConfig.ts";
19import { isNightlyDesktopVersion } from "../updates/updateChannels.ts";16import { isNightlyDesktopVersion } from "../updates/updateChannels.ts";
20 17
@@ -30,55 +27,53 @@ export interface MakeDesktopEnvironmentInput { · 9 unmodified lines
30 readonly runningUnderArm64Translation: boolean;27 readonly runningUnderArm64Translation: boolean;
31}28}
32 29
33export interface DesktopEnvironmentShape {
34 readonly path: Path.Path;
35 readonly dirname: string;
36 readonly platform: NodeJS.Platform;
37 readonly processArch: string;
38 readonly isPackaged: boolean;
39 readonly isDevelopment: boolean;
40 readonly appVersion: string;
41 readonly appPath: string;
42 readonly resourcesPath: string;
43 readonly homeDirectory: string;
44 readonly appDataDirectory: string;
45 readonly baseDir: string;
46 readonly stateDir: string;
47 readonly desktopSettingsPath: string;
48 readonly clientSettingsPath: string;
49 readonly savedEnvironmentRegistryPath: string;
50 readonly serverSettingsPath: string;
51 readonly logDir: string;
52 readonly browserArtifactsDir: string;
53 readonly rootDir: string;
54 readonly appRoot: string;
55 readonly backendEntryPath: string;
56 readonly backendCwd: string;
57 readonly preloadPath: string;
58 readonly appUpdateYmlPath: string;
59 readonly devServerUrl: Option.Option<URL>;
60 readonly devRemoteT3ServerEntryPath: Option.Option<string>;
61 readonly configuredBackendPort: Option.Option<number>;
62 readonly commitHashOverride: Option.Option<string>;
63 readonly otlpTracesUrl: Option.Option<string>;
64 readonly otlpExportIntervalMs: number;
65 readonly branding: DesktopAppBranding;
66 readonly displayName: string;
67 readonly appUserModelId: string;
68 readonly linuxDesktopEntryName: string;
69 readonly linuxWmClass: string;
70 readonly userDataDirName: string;
71 readonly legacyUserDataDirName: string;
72 readonly defaultDesktopSettings: DesktopSettings;
73 readonly runtimeInfo: DesktopRuntimeInfo;
74 readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>;
75 readonly resolveResourcePathCandidates: (fileName: string) => readonly string[];
76 readonly developmentDockIconPath: string;
77}
78
79export class DesktopEnvironment extends Context.Service<30export class DesktopEnvironment extends Context.Service<
80 DesktopEnvironment,31 DesktopEnvironment,
81 DesktopEnvironmentShape32 {
33 readonly path: Path.Path;
34 readonly dirname: string;
35 readonly platform: NodeJS.Platform;
36 readonly processArch: string;
37 readonly isPackaged: boolean;
38 readonly isDevelopment: boolean;
39 readonly appVersion: string;
40 readonly appPath: string;
41 readonly resourcesPath: string;
42 readonly homeDirectory: string;
43 readonly appDataDirectory: string;
44 readonly baseDir: string;
45 readonly stateDir: string;
46 readonly desktopSettingsPath: string;
47 readonly clientSettingsPath: string;
48 readonly savedEnvironmentRegistryPath: string;
49 readonly serverSettingsPath: string;
50 readonly logDir: string;
51 readonly browserArtifactsDir: string;
52 readonly rootDir: string;
53 readonly appRoot: string;
54 readonly backendEntryPath: string;
55 readonly backendCwd: string;
56 readonly preloadPath: string;
57 readonly appUpdateYmlPath: string;
58 readonly devServerUrl: Option.Option<URL>;
59 readonly devRemoteT3ServerEntryPath: Option.Option<string>;
60 readonly configuredBackendPort: Option.Option<number>;
61 readonly commitHashOverride: Option.Option<string>;
62 readonly otlpTracesUrl: Option.Option<string>;
63 readonly otlpExportIntervalMs: number;
64 readonly branding: DesktopAppBranding;
65 readonly displayName: string;
66 readonly appUserModelId: string;
67 readonly linuxDesktopEntryName: string;
68 readonly linuxWmClass: string;
69 readonly userDataDirName: string;
70 readonly legacyUserDataDirName: string;
71 readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings;
72 readonly runtimeInfo: DesktopRuntimeInfo;
73 readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>;
74 readonly resolveResourcePathCandidates: (fileName: string) => readonly string[];
75 readonly developmentDockIconPath: string;
76 }
82>()("@t3tools/desktop/app/DesktopEnvironment") {}77>()("@t3tools/desktop/app/DesktopEnvironment") {}
83 78
84const APP_BASE_NAME = "T3 Code";79const APP_BASE_NAME = "T3 Code";
@@ -136,9 +131,9 @@ function resolveDesktopRuntimeInfo(input: { · 51 unmodified lines
136 };131 };
137}132}
138 133
139const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* (134const make = Effect.fn("desktop.environment.make")(function* (
140 input: MakeDesktopEnvironmentInput,135 input: MakeDesktopEnvironmentInput,
141): Effect.fn.Return<DesktopEnvironmentShape, Config.ConfigError, Path.Path> {136): Effect.fn.Return<DesktopEnvironment["Service"], Config.ConfigError, Path.Path> {
142 const path = yield* Path.Path;137 const path = yield* Path.Path;
143 const config = yield* DesktopConfig.DesktopConfig;138 const config = yield* DesktopConfig.DesktopConfig;
144 const homeDirectory = input.homeDirectory;139 const homeDirectory = input.homeDirectory;
@@ -208,7 +203,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( · 63 unmodified lines
208 linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",203 linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",
209 userDataDirName,204 userDataDirName,
210 legacyUserDataDirName,205 legacyUserDataDirName,
211 defaultDesktopSettings: resolveDefaultDesktopSettings(input.appVersion),206 defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion),
212 runtimeInfo: resolveDesktopRuntimeInfo({207 runtimeInfo: resolveDesktopRuntimeInfo({
213 platform: input.platform,208 platform: input.platform,
214 processArch: input.processArch,209 processArch: input.processArch,
@@ -250,4 +245,4 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( · 35 unmodified lines
250});245});
251 246
252export const layer = (input: MakeDesktopEnvironmentInput) =>247export const layer = (input: MakeDesktopEnvironmentInput) =>
253 Layer.effect(DesktopEnvironment, makeDesktopEnvironment(input));248 Layer.effect(DesktopEnvironment, make(input));
apps/desktop/src/app/DesktopLifecycle.ts+89−120
@@ -1,7 +1,6 @@
1import * as Cause from "effect/Cause";1import * as Cause from "effect/Cause";
2import * as Context from "effect/Context";2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Deferred from "effect/Deferred";
5import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
6import * as Ref from "effect/Ref";5import * as Ref from "effect/Ref";
7import * as Scope from "effect/Scope";6import * as Scope from "effect/Scope";
@@ -10,63 +9,34 @@ import type * as Electron from "electron"; · 2 unmodified lines
10 9
11import * as DesktopEnvironment from "./DesktopEnvironment.ts";10import * as DesktopEnvironment from "./DesktopEnvironment.ts";
12import * as DesktopObservability from "./DesktopObservability.ts";11import * as DesktopObservability from "./DesktopObservability.ts";
12import * as DesktopShutdownModule from "./DesktopShutdown.ts";
13import * as ElectronApp from "../electron/ElectronApp.ts";13import * as ElectronApp from "../electron/ElectronApp.ts";
14import * as ElectronTheme from "../electron/ElectronTheme.ts";14import * as ElectronTheme from "../electron/ElectronTheme.ts";
15import * as DesktopState from "./DesktopState.ts";15import * as DesktopState from "./DesktopState.ts";
16import * as DesktopWindow from "../window/DesktopWindow.ts";16import * as DesktopWindow from "../window/DesktopWindow.ts";
17 17
18export interface DesktopShutdownShape {18export { DesktopShutdown, layer as layerShutdown } from "./DesktopShutdown.ts";
19 readonly request: Effect.Effect<void>;
20 readonly awaitRequest: Effect.Effect<void>;
21 readonly markComplete: Effect.Effect<void>;
22 readonly awaitComplete: Effect.Effect<void>;
23 readonly isComplete: Effect.Effect<boolean>;
24}
25
26export class DesktopShutdown extends Context.Service<DesktopShutdown, DesktopShutdownShape>()(
27 "@t3tools/desktop/app/DesktopLifecycle/DesktopShutdown",
28) {}
29
30const makeShutdown = Effect.gen(function* () {
31 const requested = yield* Deferred.make<void>();
32 const completed = yield* Deferred.make<void>();
33 const completedRef = yield* Ref.make(false);
34
35 return DesktopShutdown.of({
36 request: Deferred.succeed(requested, undefined).pipe(Effect.asVoid),
37 awaitRequest: Deferred.await(requested),
38 markComplete: Ref.set(completedRef, true).pipe(
39 Effect.andThen(Deferred.succeed(completed, undefined)),
40 Effect.asVoid,
41 ),
42 awaitComplete: Deferred.await(completed),
43 isComplete: Ref.get(completedRef),
44 });
45});
46
47export const layerShutdown = Layer.effect(DesktopShutdown, makeShutdown);
48 19
49export type DesktopLifecycleRuntimeServices =20export type DesktopLifecycleRuntimeServices =
50 | DesktopEnvironment.DesktopEnvironment21 | DesktopEnvironment.DesktopEnvironment
51 | DesktopShutdown22 | DesktopShutdownModule.DesktopShutdown
52 | DesktopState.DesktopState23 | DesktopState.DesktopState
53 | DesktopWindow.DesktopWindow24 | DesktopWindow.DesktopWindow
54 | ElectronApp.ElectronApp25 | ElectronApp.ElectronApp
55 | ElectronTheme.ElectronTheme;26 | ElectronTheme.ElectronTheme;
56 27
57export interface DesktopLifecycleShape {
58 readonly relaunch: (
59 reason: string,
60 ) => Effect.Effect<void, never, DesktopLifecycleRuntimeServices>;
61 readonly register: Effect.Effect<void, never, Scope.Scope | DesktopLifecycleRuntimeServices>;
62}
63
64/**28/**
65 * @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme29 * @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme
66 */30 */
67export class DesktopLifecycle extends Context.Service<DesktopLifecycle, DesktopLifecycleShape>()(31export class DesktopLifecycle extends Context.Service<
68 "@t3tools/desktop/app/DesktopLifecycle",32 DesktopLifecycle,
69) {}33 {
34 readonly relaunch: (
35 reason: string,
36 ) => Effect.Effect<void, never, DesktopLifecycleRuntimeServices>;
37 readonly register: Effect.Effect<void, never, Scope.Scope | DesktopLifecycleRuntimeServices>;
38 }
39>()("@t3tools/desktop/app/DesktopLifecycle") {}
70 40
71const { logInfo: logLifecycleInfo, logError: logLifecycleError } =41const { logInfo: logLifecycleInfo, logError: logLifecycleError } =
72 DesktopObservability.makeComponentLogger("desktop-lifecycle");42 DesktopObservability.makeComponentLogger("desktop-lifecycle");
@@ -93,8 +63,8 @@ function addScopedListener<Args extends ReadonlyArray<unknown>>( · 20 unmodified lines
93}63}
94 64
95const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")(65const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")(
96 function* (): Effect.fn.Return<void, never, DesktopShutdown> {66 function* (): Effect.fn.Return<void, never, DesktopShutdownModule.DesktopShutdown> {
97 const shutdown = yield* DesktopShutdown;67 const shutdown = yield* DesktopShutdownModule.DesktopShutdown;
98 yield* shutdown.request;68 yield* shutdown.request;
99 yield* shutdown.awaitComplete;69 yield* shutdown.awaitComplete;
100 },70 },
@@ -154,83 +124,82 @@ function quitFromSignal( · 53 unmodified lines
154 );124 );
155}125}
156 126
157export const layer = Layer.succeed(127const make = DesktopLifecycle.of({
158 DesktopLifecycle,128 relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {
159 DesktopLifecycle.of({129 const electronApp = yield* ElectronApp.ElectronApp;
160 relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {130 const environment = yield* DesktopEnvironment.DesktopEnvironment;
161 const electronApp = yield* ElectronApp.ElectronApp;131 const state = yield* DesktopState.DesktopState;
162 const environment = yield* DesktopEnvironment.DesktopEnvironment;132 yield* logLifecycleInfo("desktop relaunch requested", { reason });
163 const state = yield* DesktopState.DesktopState;133 yield* Effect.gen(function* () {
164 yield* logLifecycleInfo("desktop relaunch requested", { reason });134 yield* Effect.yieldNow;
165 yield* Effect.gen(function* () {135 yield* Ref.set(state.quitting, true);
166 yield* Effect.yieldNow;136 yield* requestDesktopShutdownAndWait();
167 yield* Ref.set(state.quitting, true);137 if (environment.isDevelopment) {
168 yield* requestDesktopShutdownAndWait();138 yield* electronApp.exit(75);
169 if (environment.isDevelopment) {139 return;
170 yield* electronApp.exit(75);140 }
171 return;141 yield* electronApp.relaunch({
172 }142 execPath: process.execPath,
173 yield* electronApp.relaunch({143 args: process.argv.slice(1),
174 execPath: process.execPath,
175 args: process.argv.slice(1),
176 });
177 yield* electronApp.exit(0);
178 }).pipe(
179 Effect.catchCause((cause) =>
180 logLifecycleError("desktop relaunch failed", {
181 cause: Cause.pretty(cause),
182 }),
183 ),
184 Effect.forkDetach,
185 Effect.asVoid,
186 );
187 }),
188 register: Effect.gen(function* () {
189 const desktopWindow = yield* DesktopWindow.DesktopWindow;
190 const electronApp = yield* ElectronApp.ElectronApp;
191 const electronTheme = yield* ElectronTheme.ElectronTheme;
192 const environment = yield* DesktopEnvironment.DesktopEnvironment;
193 const context = yield* Effect.context<DesktopLifecycleRuntimeServices>();
194 const runEffect = Effect.runPromiseWith(context);
195 let quitAllowed = false;
196 yield* electronTheme.onUpdated(() => {
197 void runEffect(
198 desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")),
199 );
200 });
201 yield* electronApp.on("before-quit", (event: Electron.Event) => {
202 handleBeforeQuit(
203 event,
204 runEffect,
205 () => quitAllowed,
206 () => {
207 quitAllowed = true;
208 },
209 );
210 });144 });
211 yield* electronApp.on("activate", () => {145 yield* electronApp.exit(0);
212 void runEffect(desktopWindow.activate.pipe(Effect.withSpan("desktop.lifecycle.activate")));146 }).pipe(
147 Effect.catchCause((cause) =>
148 logLifecycleError("desktop relaunch failed", {
149 cause: Cause.pretty(cause),
150 }),
151 ),
152 Effect.forkDetach,
153 Effect.asVoid,
154 );
155 }),
156 register: Effect.gen(function* () {
157 const desktopWindow = yield* DesktopWindow.DesktopWindow;
158 const electronApp = yield* ElectronApp.ElectronApp;
159 const electronTheme = yield* ElectronTheme.ElectronTheme;
160 const environment = yield* DesktopEnvironment.DesktopEnvironment;
161 const context = yield* Effect.context<DesktopLifecycleRuntimeServices>();
162 const runEffect = Effect.runPromiseWith(context);
163 let quitAllowed = false;
164 yield* electronTheme.onUpdated(() => {
165 void runEffect(
166 desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")),
167 );
168 });
169 yield* electronApp.on("before-quit", (event: Electron.Event) => {
170 handleBeforeQuit(
171 event,
172 runEffect,
173 () => quitAllowed,
174 () => {
175 quitAllowed = true;
176 },
177 );
178 });
179 yield* electronApp.on("activate", () => {
180 void runEffect(desktopWindow.activate.pipe(Effect.withSpan("desktop.lifecycle.activate")));
181 });
182 yield* electronApp.on("window-all-closed", () => {
183 void runEffect(
184 Effect.gen(function* () {
185 const app = yield* ElectronApp.ElectronApp;
186 const state = yield* DesktopState.DesktopState;
187 if (environment.platform !== "darwin" && !(yield* Ref.get(state.quitting))) {
188 yield* app.quit;
189 }
190 }).pipe(Effect.withSpan("desktop.lifecycle.windowAllClosed")),
191 );
192 });
193
194 if (environment.platform !== "win32") {
195 yield* addScopedListener(process, "SIGINT", () => {
196 quitFromSignal("SIGINT", runEffect);
213 });197 });
214 yield* electronApp.on("window-all-closed", () => {198 yield* addScopedListener(process, "SIGTERM", () => {
215 void runEffect(199 quitFromSignal("SIGTERM", runEffect);
216 Effect.gen(function* () {
217 const app = yield* ElectronApp.ElectronApp;
218 const state = yield* DesktopState.DesktopState;
219 if (environment.platform !== "darwin" && !(yield* Ref.get(state.quitting))) {
220 yield* app.quit;
221 }
222 }).pipe(Effect.withSpan("desktop.lifecycle.windowAllClosed")),
223 );
224 });200 });
201 }
202 }).pipe(Effect.withSpan("desktop.lifecycle.register")),
203});
225 204
226 if (environment.platform !== "win32") {205export const layer = Layer.succeed(DesktopLifecycle, make);
227 yield* addScopedListener(process, "SIGINT", () => {
228 quitFromSignal("SIGINT", runEffect);
229 });
230 yield* addScopedListener(process, "SIGTERM", () => {
231 quitFromSignal("SIGTERM", runEffect);
232 });
233 }
234 }).pipe(Effect.withSpan("desktop.lifecycle.register")),
235 }),
236);
apps/desktop/src/app/DesktopObservability.ts+3−3
@@ -314,7 +314,7 @@ const PRIMARY_BACKEND_LOG_INSTANCE_ID = PRIMARY_LOCAL_ENVIRONMENT_ID; · 313 unmodified lines
314const sanitizeInstanceIdForFileName = (id: string): string => id.replace(/[^a-zA-Z0-9._-]+/g, "_");314const sanitizeInstanceIdForFileName = (id: string): string => id.replace(/[^a-zA-Z0-9._-]+/g, "_");
315 315
316const backendLogFilePathForInstance = (316const backendLogFilePathForInstance = (
317 environment: DesktopEnvironment.DesktopEnvironmentShape,317 environment: DesktopEnvironment.DesktopEnvironment["Service"],
318 id: string,318 id: string,
319): string => {319): string => {
320 // Primary keeps the historical "server-child.log" path so ops scripts320 // Primary keeps the historical "server-child.log" path so ops scripts
@@ -332,7 +332,7 @@ const backendLogFilePathForInstance = ( · 11 unmodified lines
332// per-call shape lets the shape annotate writes with the *caller's*332// per-call shape lets the shape annotate writes with the *caller's*
333// id rather than whatever id created the cached writer first.333// id rather than whatever id created the cached writer first.
334const makeBackendOutputSinkForInstance = (334const makeBackendOutputSinkForInstance = (
335 environment: DesktopEnvironment.DesktopEnvironmentShape,335 environment: DesktopEnvironment.DesktopEnvironment["Service"],
336 id: string,336 id: string,
337): Effect.Effect<337): Effect.Effect<
338 Option.Option<RotatingLogFileWriter>,338 Option.Option<RotatingLogFileWriter>,
@@ -344,7 +344,7 @@ const makeBackendOutputSinkForInstance = ( · 5 unmodified lines
344 }).pipe(Effect.option);344 }).pipe(Effect.option);
345 345
346const makeBackendOutputLogShape = (346const makeBackendOutputLogShape = (
347 environment: DesktopEnvironment.DesktopEnvironmentShape,347 environment: DesktopEnvironment.DesktopEnvironment["Service"],
348 id: string,348 id: string,
349 sink: Option.Option<RotatingLogFileWriter>,349 sink: Option.Option<RotatingLogFileWriter>,
350): DesktopBackendOutputLogShape =>350): DesktopBackendOutputLogShape =>
apps/desktop/src/app/DesktopShutdown.ts+35−0
@@ -0,0 +1,35 @@
1import * as Context from "effect/Context";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Ref from "effect/Ref";
6
7export class DesktopShutdown extends Context.Service<
8 DesktopShutdown,
9 {
10 readonly request: Effect.Effect<void>;
11 readonly awaitRequest: Effect.Effect<void>;
12 readonly markComplete: Effect.Effect<void>;
13 readonly awaitComplete: Effect.Effect<void>;
14 readonly isComplete: Effect.Effect<boolean>;
15 }
16>()("@t3tools/desktop/app/DesktopShutdown") {}
17
18const make = Effect.gen(function* () {
19 const requested = yield* Deferred.make<void>();
20 const completed = yield* Deferred.make<void>();
21 const completedRef = yield* Ref.make(false);
22
23 return DesktopShutdown.of({
24 request: Deferred.succeed(requested, undefined).pipe(Effect.asVoid),
25 awaitRequest: Deferred.await(requested),
26 markComplete: Ref.set(completedRef, true).pipe(
27 Effect.andThen(Deferred.succeed(completed, undefined)),
28 Effect.asVoid,
29 ),
30 awaitComplete: Deferred.await(completed),
31 isComplete: Ref.get(completedRef),
32 });
33});
34
35export const layer = Layer.effect(DesktopShutdown, make);
apps/desktop/src/app/DesktopState.ts+12−12
@@ -3,17 +3,17 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as Ref from "effect/Ref";4import * as Ref from "effect/Ref";
5 5
6export interface DesktopStateShape {6export class DesktopState extends Context.Service<
7 readonly quitting: Ref.Ref<boolean>;7 DesktopState,
8}8 {
9 readonly backendReady: Ref.Ref<boolean>;
10 readonly quitting: Ref.Ref<boolean>;
11 }
12>()("@t3tools/desktop/app/DesktopState") {}
9 13
10export class DesktopState extends Context.Service<DesktopState, DesktopStateShape>()(14const make = Effect.all({
11 "@t3tools/desktop/app/DesktopState",15 backendReady: Ref.make(false),
12) {}16 quitting: Ref.make(false),
17});
13 18
14export const layer = Layer.effect(19export const layer = Layer.effect(DesktopState, make);
15 DesktopState,
16 Effect.all({
17 quitting: Ref.make(false),
18 }),
19);
apps/desktop/src/ipc/DesktopIpc.ts+16−14
@@ -1,5 +1,6 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
4import * as Scope from "effect/Scope";5import * as Scope from "effect/Scope";
5 6
@@ -33,20 +34,19 @@ export interface DesktopSyncIpcMethod<E, R> { · 27 unmodified lines
33 readonly handler: () => Effect.Effect<unknown, E, R>;34 readonly handler: () => Effect.Effect<unknown, E, R>;
34}35}
35 36
36export interface DesktopIpcShape {37export class DesktopIpc extends Context.Service<
37 readonly handle: <E, R>(38 DesktopIpc,
38 input: DesktopIpcMethod<E, R>,39 {
39 ) => Effect.Effect<void, never, R | Scope.Scope>;40 readonly handle: <E, R>(
40 readonly handleSync: <E, R>(41 input: DesktopIpcMethod<E, R>,
41 input: DesktopSyncIpcMethod<E, R>,42 ) => Effect.Effect<void, never, R | Scope.Scope>;
42 ) => Effect.Effect<void, never, R | Scope.Scope>;43 readonly handleSync: <E, R>(
43}44 input: DesktopSyncIpcMethod<E, R>,
44 45 ) => Effect.Effect<void, never, R | Scope.Scope>;
45export class DesktopIpc extends Context.Service<DesktopIpc, DesktopIpcShape>()(46 }
46 "@t3tools/desktop/ipc/DesktopIpc",47>()("@t3tools/desktop/ipc/DesktopIpc") {}
47) {}48
48 49export const make = (ipcMain: DesktopIpcMain): DesktopIpc["Service"] =>
49export const make = (ipcMain: DesktopIpcMain): DesktopIpcShape =>
50 DesktopIpc.of({50 DesktopIpc.of({
51 handle: Effect.fn("desktop.ipc.registerInvoke")(function* <E, R>({51 handle: Effect.fn("desktop.ipc.registerInvoke")(function* <E, R>({
52 channel,52 channel,
@@ -97,6 +97,8 @@ export const make = (ipcMain: DesktopIpcMain): DesktopIpcShape => · 44 unmodified lines
97 }),97 }),
98 });98 });
99 99
100export const layer = (ipcMain: DesktopIpcMain) => Layer.succeed(DesktopIpc, make(ipcMain));
101
100/**102/**
101 * Convenience helpers for creating IPC methods103 * Convenience helpers for creating IPC methods
102 */104 */
apps/desktop/src/ipc/methods/clientSettings.ts+3−3
@@ -5,9 +5,9 @@ import * as Schema from "effect/Schema"; · 4 unmodified lines
5 5
6import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";6import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";
7import * as IpcChannels from "../channels.ts";7import * as IpcChannels from "../channels.ts";
8import { makeIpcMethod } from "../DesktopIpc.ts";8import * as DesktopIpc from "../DesktopIpc.ts";
9 9
10export const getClientSettings = makeIpcMethod({10export const getClientSettings = DesktopIpc.makeIpcMethod({
11 channel: IpcChannels.GET_CLIENT_SETTINGS_CHANNEL,11 channel: IpcChannels.GET_CLIENT_SETTINGS_CHANNEL,
12 payload: Schema.Void,12 payload: Schema.Void,
13 result: Schema.NullOr(ClientSettingsSchema),13 result: Schema.NullOr(ClientSettingsSchema),
@@ -17,7 +17,7 @@ export const getClientSettings = makeIpcMethod({ · 3 unmodified lines
17 }),17 }),
18});18});
19 19
20export const setClientSettings = makeIpcMethod({20export const setClientSettings = DesktopIpc.makeIpcMethod({
21 channel: IpcChannels.SET_CLIENT_SETTINGS_CHANNEL,21 channel: IpcChannels.SET_CLIENT_SETTINGS_CHANNEL,
22 payload: ClientSettingsSchema,22 payload: ClientSettingsSchema,
23 result: Schema.Void,23 result: Schema.Void,
apps/desktop/src/ipc/methods/connectionCatalog.ts+4−4
@@ -4,9 +4,9 @@ import * as Schema from "effect/Schema"; · 3 unmodified lines
4 4
5import * as DesktopConnectionCatalogStore from "../../app/DesktopConnectionCatalogStore.ts";5import * as DesktopConnectionCatalogStore from "../../app/DesktopConnectionCatalogStore.ts";
6import * as IpcChannels from "../channels.ts";6import * as IpcChannels from "../channels.ts";
7import { makeIpcMethod } from "../DesktopIpc.ts";7import * as DesktopIpc from "../DesktopIpc.ts";
8 8
9export const getConnectionCatalog = makeIpcMethod({9export const getConnectionCatalog = DesktopIpc.makeIpcMethod({
10 channel: IpcChannels.GET_CONNECTION_CATALOG_CHANNEL,10 channel: IpcChannels.GET_CONNECTION_CATALOG_CHANNEL,
11 payload: Schema.Void,11 payload: Schema.Void,
12 result: Schema.NullOr(Schema.String),12 result: Schema.NullOr(Schema.String),
@@ -16,7 +16,7 @@ export const getConnectionCatalog = makeIpcMethod({ · 3 unmodified lines
16 }),16 }),
17});17});
18 18
19export const setConnectionCatalog = makeIpcMethod({19export const setConnectionCatalog = DesktopIpc.makeIpcMethod({
20 channel: IpcChannels.SET_CONNECTION_CATALOG_CHANNEL,20 channel: IpcChannels.SET_CONNECTION_CATALOG_CHANNEL,
21 payload: Schema.String,21 payload: Schema.String,
22 result: Schema.Boolean,22 result: Schema.Boolean,
@@ -26,7 +26,7 @@ export const setConnectionCatalog = makeIpcMethod({ · 3 unmodified lines
26 }),26 }),
27});27});
28 28
29export const clearConnectionCatalog = makeIpcMethod({29export const clearConnectionCatalog = DesktopIpc.makeIpcMethod({
30 channel: IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL,30 channel: IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL,
31 payload: Schema.Void,31 payload: Schema.Void,
32 result: Schema.Void,32 result: Schema.Void,
apps/desktop/src/ipc/methods/preview.ts+23−23
@@ -27,7 +27,7 @@ import { pathToFileURL } from "node:url"; · 26 unmodified lines
27import * as PreviewManager from "../../preview/Manager.ts";27import * as PreviewManager from "../../preview/Manager.ts";
28import { PREVIEW_WEBVIEW_PREFERENCES } from "../../preview/WebviewPreferences.ts";28import { PREVIEW_WEBVIEW_PREFERENCES } from "../../preview/WebviewPreferences.ts";
29import * as IpcChannels from "../channels.ts";29import * as IpcChannels from "../channels.ts";
30import { makeIpcMethod } from "../DesktopIpc.ts";30import * as DesktopIpc from "../DesktopIpc.ts";
31 31
32const broadcast = (channel: string, ...args: ReadonlyArray<unknown>): void => {32const broadcast = (channel: string, ...args: ReadonlyArray<unknown>): void => {
33 for (const window of BrowserWindow.getAllWindows()) {33 for (const window of BrowserWindow.getAllWindows()) {
@@ -52,7 +52,7 @@ export const installPreviewEventForwarding = Effect.fn( · 18 unmodified lines
52 });52 });
53});53});
54 54
55export const createTab = makeIpcMethod({55export const createTab = DesktopIpc.makeIpcMethod({
56 channel: IpcChannels.PREVIEW_CREATE_TAB_CHANNEL,56 channel: IpcChannels.PREVIEW_CREATE_TAB_CHANNEL,
57 payload: DesktopPreviewTabInputSchema,57 payload: DesktopPreviewTabInputSchema,
58 result: Schema.Void,58 result: Schema.Void,
@@ -62,7 +62,7 @@ export const createTab = makeIpcMethod({ · 3 unmodified lines
62 }),62 }),
63});63});
64 64
65export const closeTab = makeIpcMethod({65export const closeTab = DesktopIpc.makeIpcMethod({
66 channel: IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL,66 channel: IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL,
67 payload: DesktopPreviewTabInputSchema,67 payload: DesktopPreviewTabInputSchema,
68 result: Schema.Void,68 result: Schema.Void,
@@ -72,7 +72,7 @@ export const closeTab = makeIpcMethod({ · 3 unmodified lines
72 }),72 }),
73});73});
74 74
75export const registerWebview = makeIpcMethod({75export const registerWebview = DesktopIpc.makeIpcMethod({
76 channel: IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL,76 channel: IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL,
77 payload: DesktopPreviewRegisterWebviewInputSchema,77 payload: DesktopPreviewRegisterWebviewInputSchema,
78 result: Schema.Void,78 result: Schema.Void,
@@ -82,7 +82,7 @@ export const registerWebview = makeIpcMethod({ · 3 unmodified lines
82 }),82 }),
83});83});
84 84
85export const navigate = makeIpcMethod({85export const navigate = DesktopIpc.makeIpcMethod({
86 channel: IpcChannels.PREVIEW_NAVIGATE_CHANNEL,86 channel: IpcChannels.PREVIEW_NAVIGATE_CHANNEL,
87 payload: DesktopPreviewNavigateInputSchema,87 payload: DesktopPreviewNavigateInputSchema,
88 result: Schema.Void,88 result: Schema.Void,
@@ -100,7 +100,7 @@ const tabMethod = ( · 11 unmodified lines
100 tabId: string,100 tabId: string,
101 ) => Effect.Effect<void, PreviewManager.PreviewManagerError>,101 ) => Effect.Effect<void, PreviewManager.PreviewManagerError>,
102) =>102) =>
103 makeIpcMethod({103 DesktopIpc.makeIpcMethod({
104 channel,104 channel,
105 payload: DesktopPreviewTabInputSchema,105 payload: DesktopPreviewTabInputSchema,
106 result: Schema.Void,106 result: Schema.Void,
@@ -166,7 +166,7 @@ export const stopRecording = tabMethod( · 59 unmodified lines
166 (manager, tabId) => manager.stopRecording(tabId),166 (manager, tabId) => manager.stopRecording(tabId),
167);167);
168 168
169export const clearCookies = makeIpcMethod({169export const clearCookies = DesktopIpc.makeIpcMethod({
170 channel: IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL,170 channel: IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL,
171 payload: Schema.Void,171 payload: Schema.Void,
172 result: Schema.Void,172 result: Schema.Void,
@@ -176,7 +176,7 @@ export const clearCookies = makeIpcMethod({ · 3 unmodified lines
176 }),176 }),
177});177});
178 178
179export const clearCache = makeIpcMethod({179export const clearCache = DesktopIpc.makeIpcMethod({
180 channel: IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL,180 channel: IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL,
181 payload: Schema.Void,181 payload: Schema.Void,
182 result: Schema.Void,182 result: Schema.Void,
@@ -186,7 +186,7 @@ export const clearCache = makeIpcMethod({ · 3 unmodified lines
186 }),186 }),
187});187});
188 188
189export const getPreviewConfig = makeIpcMethod({189export const getPreviewConfig = DesktopIpc.makeIpcMethod({
190 channel: IpcChannels.PREVIEW_GET_CONFIG_CHANNEL,190 channel: IpcChannels.PREVIEW_GET_CONFIG_CHANNEL,
191 payload: DesktopPreviewConfigInputSchema,191 payload: DesktopPreviewConfigInputSchema,
192 result: DesktopPreviewWebviewConfigSchema,192 result: DesktopPreviewWebviewConfigSchema,
@@ -201,7 +201,7 @@ export const getPreviewConfig = makeIpcMethod({ · 8 unmodified lines
201 }),201 }),
202});202});
203 203
204export const setAnnotationTheme = makeIpcMethod({204export const setAnnotationTheme = DesktopIpc.makeIpcMethod({
205 channel: IpcChannels.PREVIEW_SET_ANNOTATION_THEME_CHANNEL,205 channel: IpcChannels.PREVIEW_SET_ANNOTATION_THEME_CHANNEL,
206 payload: DesktopPreviewAnnotationThemeInputSchema,206 payload: DesktopPreviewAnnotationThemeInputSchema,
207 result: Schema.Void,207 result: Schema.Void,
@@ -211,7 +211,7 @@ export const setAnnotationTheme = makeIpcMethod({ · 3 unmodified lines
211 }),211 }),
212});212});
213 213
214export const pickElement = makeIpcMethod({214export const pickElement = DesktopIpc.makeIpcMethod({
215 channel: IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL,215 channel: IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL,
216 payload: DesktopPreviewTabInputSchema,216 payload: DesktopPreviewTabInputSchema,
217 result: Schema.NullOr(PreviewAnnotationPayloadSchema),217 result: Schema.NullOr(PreviewAnnotationPayloadSchema),
@@ -221,7 +221,7 @@ export const pickElement = makeIpcMethod({ · 3 unmodified lines
221 }),221 }),
222});222});
223 223
224export const captureScreenshot = makeIpcMethod({224export const captureScreenshot = DesktopIpc.makeIpcMethod({
225 channel: IpcChannels.PREVIEW_CAPTURE_SCREENSHOT_CHANNEL,225 channel: IpcChannels.PREVIEW_CAPTURE_SCREENSHOT_CHANNEL,
226 payload: DesktopPreviewTabInputSchema,226 payload: DesktopPreviewTabInputSchema,
227 result: DesktopPreviewScreenshotArtifactSchema,227 result: DesktopPreviewScreenshotArtifactSchema,
@@ -231,7 +231,7 @@ export const captureScreenshot = makeIpcMethod({ · 3 unmodified lines
231 }),231 }),
232});232});
233 233
234export const revealArtifact = makeIpcMethod({234export const revealArtifact = DesktopIpc.makeIpcMethod({
235 channel: IpcChannels.PREVIEW_REVEAL_ARTIFACT_CHANNEL,235 channel: IpcChannels.PREVIEW_REVEAL_ARTIFACT_CHANNEL,
236 payload: DesktopPreviewArtifactInputSchema,236 payload: DesktopPreviewArtifactInputSchema,
237 result: Schema.Void,237 result: Schema.Void,
@@ -241,7 +241,7 @@ export const revealArtifact = makeIpcMethod({ · 3 unmodified lines
241 }),241 }),
242});242});
243 243
244export const copyArtifactToClipboard = makeIpcMethod({244export const copyArtifactToClipboard = DesktopIpc.makeIpcMethod({
245 channel: IpcChannels.PREVIEW_COPY_ARTIFACT_CHANNEL,245 channel: IpcChannels.PREVIEW_COPY_ARTIFACT_CHANNEL,
246 payload: DesktopPreviewArtifactInputSchema,246 payload: DesktopPreviewArtifactInputSchema,
247 result: Schema.Void,247 result: Schema.Void,
@@ -251,7 +251,7 @@ export const copyArtifactToClipboard = makeIpcMethod({ · 3 unmodified lines
251 }),251 }),
252});252});
253 253
254export const automationStatus = makeIpcMethod({254export const automationStatus = DesktopIpc.makeIpcMethod({
255 channel: IpcChannels.PREVIEW_AUTOMATION_STATUS_CHANNEL,255 channel: IpcChannels.PREVIEW_AUTOMATION_STATUS_CHANNEL,
256 payload: DesktopPreviewTabInputSchema,256 payload: DesktopPreviewTabInputSchema,
257 result: PreviewAutomationStatus,257 result: PreviewAutomationStatus,
@@ -261,7 +261,7 @@ export const automationStatus = makeIpcMethod({ · 3 unmodified lines
261 }),261 }),
262});262});
263 263
264export const automationSnapshot = makeIpcMethod({264export const automationSnapshot = DesktopIpc.makeIpcMethod({
265 channel: IpcChannels.PREVIEW_AUTOMATION_SNAPSHOT_CHANNEL,265 channel: IpcChannels.PREVIEW_AUTOMATION_SNAPSHOT_CHANNEL,
266 payload: DesktopPreviewTabInputSchema,266 payload: DesktopPreviewTabInputSchema,
267 result: PreviewAutomationSnapshot,267 result: PreviewAutomationSnapshot,
@@ -271,7 +271,7 @@ export const automationSnapshot = makeIpcMethod({ · 3 unmodified lines
271 }),271 }),
272});272});
273 273
274export const automationClick = makeIpcMethod({274export const automationClick = DesktopIpc.makeIpcMethod({
275 channel: IpcChannels.PREVIEW_AUTOMATION_CLICK_CHANNEL,275 channel: IpcChannels.PREVIEW_AUTOMATION_CLICK_CHANNEL,
276 payload: DesktopPreviewAutomationClickInputSchema,276 payload: DesktopPreviewAutomationClickInputSchema,
277 result: Schema.Void,277 result: Schema.Void,
@@ -281,7 +281,7 @@ export const automationClick = makeIpcMethod({ · 3 unmodified lines
281 }),281 }),
282});282});
283 283
284export const automationType = makeIpcMethod({284export const automationType = DesktopIpc.makeIpcMethod({
285 channel: IpcChannels.PREVIEW_AUTOMATION_TYPE_CHANNEL,285 channel: IpcChannels.PREVIEW_AUTOMATION_TYPE_CHANNEL,
286 payload: DesktopPreviewAutomationTypeInputSchema,286 payload: DesktopPreviewAutomationTypeInputSchema,
287 result: Schema.Void,287 result: Schema.Void,
@@ -291,7 +291,7 @@ export const automationType = makeIpcMethod({ · 3 unmodified lines
291 }),291 }),
292});292});
293 293
294export const automationPress = makeIpcMethod({294export const automationPress = DesktopIpc.makeIpcMethod({
295 channel: IpcChannels.PREVIEW_AUTOMATION_PRESS_CHANNEL,295 channel: IpcChannels.PREVIEW_AUTOMATION_PRESS_CHANNEL,
296 payload: DesktopPreviewAutomationPressInputSchema,296 payload: DesktopPreviewAutomationPressInputSchema,
297 result: Schema.Void,297 result: Schema.Void,
@@ -301,7 +301,7 @@ export const automationPress = makeIpcMethod({ · 3 unmodified lines
301 }),301 }),
302});302});
303 303
304export const automationScroll = makeIpcMethod({304export const automationScroll = DesktopIpc.makeIpcMethod({
305 channel: IpcChannels.PREVIEW_AUTOMATION_SCROLL_CHANNEL,305 channel: IpcChannels.PREVIEW_AUTOMATION_SCROLL_CHANNEL,
306 payload: DesktopPreviewAutomationScrollInputSchema,306 payload: DesktopPreviewAutomationScrollInputSchema,
307 result: Schema.Void,307 result: Schema.Void,
@@ -311,7 +311,7 @@ export const automationScroll = makeIpcMethod({ · 3 unmodified lines
311 }),311 }),
312});312});
313 313
314export const automationEvaluate = makeIpcMethod({314export const automationEvaluate = DesktopIpc.makeIpcMethod({
315 channel: IpcChannels.PREVIEW_AUTOMATION_EVALUATE_CHANNEL,315 channel: IpcChannels.PREVIEW_AUTOMATION_EVALUATE_CHANNEL,
316 payload: DesktopPreviewAutomationEvaluateInputSchema,316 payload: DesktopPreviewAutomationEvaluateInputSchema,
317 result: Schema.Unknown,317 result: Schema.Unknown,
@@ -321,7 +321,7 @@ export const automationEvaluate = makeIpcMethod({ · 3 unmodified lines
321 }),321 }),
322});322});
323 323
324export const automationWaitFor = makeIpcMethod({324export const automationWaitFor = DesktopIpc.makeIpcMethod({
325 channel: IpcChannels.PREVIEW_AUTOMATION_WAIT_FOR_CHANNEL,325 channel: IpcChannels.PREVIEW_AUTOMATION_WAIT_FOR_CHANNEL,
326 payload: DesktopPreviewAutomationWaitForInputSchema,326 payload: DesktopPreviewAutomationWaitForInputSchema,
327 result: Schema.Void,327 result: Schema.Void,
@@ -331,7 +331,7 @@ export const automationWaitFor = makeIpcMethod({ · 3 unmodified lines
331 }),331 }),
332});332});
333 333
334export const saveRecording = makeIpcMethod({334export const saveRecording = DesktopIpc.makeIpcMethod({
335 channel: IpcChannels.PREVIEW_RECORDING_SAVE_CHANNEL,335 channel: IpcChannels.PREVIEW_RECORDING_SAVE_CHANNEL,
336 payload: DesktopPreviewRecordingSaveInputSchema,336 payload: DesktopPreviewRecordingSaveInputSchema,
337 result: DesktopPreviewRecordingArtifactSchema,337 result: DesktopPreviewRecordingArtifactSchema,
apps/desktop/src/ipc/methods/serverExposure.ts+5−5
@@ -9,14 +9,14 @@ import * as Schema from "effect/Schema"; · 8 unmodified lines
9import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";9import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
10import * as DesktopServerExposure from "../../backend/DesktopServerExposure.ts";10import * as DesktopServerExposure from "../../backend/DesktopServerExposure.ts";
11import * as IpcChannels from "../channels.ts";11import * as IpcChannels from "../channels.ts";
12import { makeIpcMethod } from "../DesktopIpc.ts";12import * as DesktopIpc from "../DesktopIpc.ts";
13 13
14const SetTailscaleServeEnabledInput = Schema.Struct({14const SetTailscaleServeEnabledInput = Schema.Struct({
15 enabled: Schema.Boolean,15 enabled: Schema.Boolean,
16 port: Schema.optionalKey(Schema.Number),16 port: Schema.optionalKey(Schema.Number),
17});17});
18 18
19export const getServerExposureState = makeIpcMethod({19export const getServerExposureState = DesktopIpc.makeIpcMethod({
20 channel: IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL,20 channel: IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL,
21 payload: Schema.Void,21 payload: Schema.Void,
22 result: DesktopServerExposureStateSchema,22 result: DesktopServerExposureStateSchema,
@@ -26,7 +26,7 @@ export const getServerExposureState = makeIpcMethod({ · 3 unmodified lines
26 }),26 }),
27});27});
28 28
29export const setServerExposureMode = makeIpcMethod({29export const setServerExposureMode = DesktopIpc.makeIpcMethod({
30 channel: IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL,30 channel: IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL,
31 payload: DesktopServerExposureModeSchema,31 payload: DesktopServerExposureModeSchema,
32 result: DesktopServerExposureStateSchema,32 result: DesktopServerExposureStateSchema,
@@ -41,7 +41,7 @@ export const setServerExposureMode = makeIpcMethod({ · 8 unmodified lines
41 }),41 }),
42});42});
43 43
44export const setTailscaleServeEnabled = makeIpcMethod({44export const setTailscaleServeEnabled = DesktopIpc.makeIpcMethod({
45 channel: IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL,45 channel: IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL,
46 payload: SetTailscaleServeEnabledInput,46 payload: SetTailscaleServeEnabledInput,
47 result: DesktopServerExposureStateSchema,47 result: DesktopServerExposureStateSchema,
@@ -58,7 +58,7 @@ export const setTailscaleServeEnabled = makeIpcMethod({ · 10 unmodified lines
58 }),58 }),
59});59});
60 60
61export const getAdvertisedEndpoints = makeIpcMethod({61export const getAdvertisedEndpoints = DesktopIpc.makeIpcMethod({
62 channel: IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL,62 channel: IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL,
63 payload: Schema.Void,63 payload: Schema.Void,
64 result: Schema.Array(AdvertisedEndpoint),64 result: Schema.Array(AdvertisedEndpoint),
apps/desktop/src/ipc/methods/sshEnvironment.ts+9−9
@@ -33,7 +33,7 @@ import * as Effect from "effect/Effect"; · 32 unmodified lines
33import * as Schema from "effect/Schema";33import * as Schema from "effect/Schema";
34 34
35import * as IpcChannels from "../channels.ts";35import * as IpcChannels from "../channels.ts";
36import { makeIpcMethod } from "../DesktopIpc.ts";36import * as DesktopIpc from "../DesktopIpc.ts";
37import * as DesktopSshEnvironment from "../../ssh/DesktopSshEnvironment.ts";37import * as DesktopSshEnvironment from "../../ssh/DesktopSshEnvironment.ts";
38import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts.ts";38import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts.ts";
39 39
@@ -107,7 +107,7 @@ const withLoopbackSshApi = · 67 unmodified lines
107 ),107 ),
108 );108 );
109 109
110export const discoverSshHosts = makeIpcMethod({110export const discoverSshHosts = DesktopIpc.makeIpcMethod({
111 channel: IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL,111 channel: IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL,
112 payload: Schema.Void,112 payload: Schema.Void,
113 result: Schema.Array(DesktopDiscoveredSshHostSchema),113 result: Schema.Array(DesktopDiscoveredSshHostSchema),
@@ -117,7 +117,7 @@ export const discoverSshHosts = makeIpcMethod({ · 3 unmodified lines
117 }),117 }),
118});118});
119 119
120export const ensureSshEnvironment = makeIpcMethod({120export const ensureSshEnvironment = DesktopIpc.makeIpcMethod({
121 channel: IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL,121 channel: IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL,
122 payload: DesktopSshEnvironmentEnsureInputSchema,122 payload: DesktopSshEnvironmentEnsureInputSchema,
123 result: DesktopSshEnvironmentEnsureResultSchema,123 result: DesktopSshEnvironmentEnsureResultSchema,
@@ -139,7 +139,7 @@ export const ensureSshEnvironment = makeIpcMethod({ · 15 unmodified lines
139 }),139 }),
140});140});
141 141
142export const disconnectSshEnvironment = makeIpcMethod({142export const disconnectSshEnvironment = DesktopIpc.makeIpcMethod({
143 channel: IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL,143 channel: IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL,
144 payload: DesktopSshEnvironmentTargetSchema,144 payload: DesktopSshEnvironmentTargetSchema,
145 result: Schema.Void,145 result: Schema.Void,
@@ -149,7 +149,7 @@ export const disconnectSshEnvironment = makeIpcMethod({ · 3 unmodified lines
149 }),149 }),
150});150});
151 151
152export const fetchSshEnvironmentDescriptor = makeIpcMethod({152export const fetchSshEnvironmentDescriptor = DesktopIpc.makeIpcMethod({
153 channel: IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL,153 channel: IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL,
154 payload: DesktopSshHttpBaseUrlInputSchema,154 payload: DesktopSshHttpBaseUrlInputSchema,
155 result: ExecutionEnvironmentDescriptor,155 result: ExecutionEnvironmentDescriptor,
@@ -160,7 +160,7 @@ export const fetchSshEnvironmentDescriptor = makeIpcMethod({ · 4 unmodified lines
160 }),160 }),
161});161});
162 162
163export const bootstrapSshBearerSession = makeIpcMethod({163export const bootstrapSshBearerSession = DesktopIpc.makeIpcMethod({
164 channel: IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL,164 channel: IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL,
165 payload: DesktopSshBearerBootstrapInputSchema,165 payload: DesktopSshBearerBootstrapInputSchema,
166 result: AuthAccessTokenResult,166 result: AuthAccessTokenResult,
@@ -177,7 +177,7 @@ export const bootstrapSshBearerSession = makeIpcMethod({ · 10 unmodified lines
177 }),177 }),
178});178});
179 179
180export const fetchSshSessionState = makeIpcMethod({180export const fetchSshSessionState = DesktopIpc.makeIpcMethod({
181 channel: IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL,181 channel: IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL,
182 payload: DesktopSshBearerRequestInputSchema,182 payload: DesktopSshBearerRequestInputSchema,
183 result: AuthSessionState,183 result: AuthSessionState,
@@ -194,7 +194,7 @@ export const fetchSshSessionState = makeIpcMethod({ · 10 unmodified lines
194 }),194 }),
195});195});
196 196
197export const issueSshWebSocketTicket = makeIpcMethod({197export const issueSshWebSocketTicket = DesktopIpc.makeIpcMethod({
198 channel: IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL,198 channel: IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL,
199 payload: DesktopSshBearerRequestInputSchema,199 payload: DesktopSshBearerRequestInputSchema,
200 result: AuthWebSocketTicketResult,200 result: AuthWebSocketTicketResult,
@@ -211,7 +211,7 @@ export const issueSshWebSocketTicket = makeIpcMethod({ · 10 unmodified lines
211 }),211 }),
212});212});
213 213
214export const resolveSshPasswordPrompt = makeIpcMethod({214export const resolveSshPasswordPrompt = DesktopIpc.makeIpcMethod({
215 channel: IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL,215 channel: IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL,
216 payload: DesktopSshPasswordPromptResolutionInputSchema,216 payload: DesktopSshPasswordPromptResolutionInputSchema,
217 result: Schema.Void,217 result: Schema.Void,
apps/desktop/src/ipc/methods/updates.ts+6−6
@@ -9,9 +9,9 @@ import * as Schema from "effect/Schema"; · 8 unmodified lines
9 9
10import * as DesktopUpdates from "../../updates/DesktopUpdates.ts";10import * as DesktopUpdates from "../../updates/DesktopUpdates.ts";
11import * as IpcChannels from "../channels.ts";11import * as IpcChannels from "../channels.ts";
12import { makeIpcMethod } from "../DesktopIpc.ts";12import * as DesktopIpc from "../DesktopIpc.ts";
13 13
14export const getUpdateState = makeIpcMethod({14export const getUpdateState = DesktopIpc.makeIpcMethod({
15 channel: IpcChannels.UPDATE_GET_STATE_CHANNEL,15 channel: IpcChannels.UPDATE_GET_STATE_CHANNEL,
16 payload: Schema.Void,16 payload: Schema.Void,
17 result: DesktopUpdateStateSchema,17 result: DesktopUpdateStateSchema,
@@ -21,7 +21,7 @@ export const getUpdateState = makeIpcMethod({ · 3 unmodified lines
21 }),21 }),
22});22});
23 23
24export const setUpdateChannel = makeIpcMethod({24export const setUpdateChannel = DesktopIpc.makeIpcMethod({
25 channel: IpcChannels.UPDATE_SET_CHANNEL_CHANNEL,25 channel: IpcChannels.UPDATE_SET_CHANNEL_CHANNEL,
26 payload: DesktopUpdateChannelSchema,26 payload: DesktopUpdateChannelSchema,
27 result: DesktopUpdateStateSchema,27 result: DesktopUpdateStateSchema,
@@ -31,7 +31,7 @@ export const setUpdateChannel = makeIpcMethod({ · 3 unmodified lines
31 }),31 }),
32});32});
33 33
34export const downloadUpdate = makeIpcMethod({34export const downloadUpdate = DesktopIpc.makeIpcMethod({
35 channel: IpcChannels.UPDATE_DOWNLOAD_CHANNEL,35 channel: IpcChannels.UPDATE_DOWNLOAD_CHANNEL,
36 payload: Schema.Void,36 payload: Schema.Void,
37 result: DesktopUpdateActionResultSchema,37 result: DesktopUpdateActionResultSchema,
@@ -41,7 +41,7 @@ export const downloadUpdate = makeIpcMethod({ · 3 unmodified lines
41 }),41 }),
42});42});
43 43
44export const installUpdate = makeIpcMethod({44export const installUpdate = DesktopIpc.makeIpcMethod({
45 channel: IpcChannels.UPDATE_INSTALL_CHANNEL,45 channel: IpcChannels.UPDATE_INSTALL_CHANNEL,
46 payload: Schema.Void,46 payload: Schema.Void,
47 result: DesktopUpdateActionResultSchema,47 result: DesktopUpdateActionResultSchema,
@@ -51,7 +51,7 @@ export const installUpdate = makeIpcMethod({ · 3 unmodified lines
51 }),51 }),
52});52});
53 53
54export const checkForUpdate = makeIpcMethod({54export const checkForUpdate = DesktopIpc.makeIpcMethod({
55 channel: IpcChannels.UPDATE_CHECK_CHANNEL,55 channel: IpcChannels.UPDATE_CHECK_CHANNEL,
56 payload: Schema.Void,56 payload: Schema.Void,
57 result: DesktopUpdateCheckResultSchema,57 result: DesktopUpdateCheckResultSchema,
apps/desktop/src/ipc/methods/window.ts+9−9
@@ -23,7 +23,7 @@ import * as ElectronShell from "../../electron/ElectronShell.ts"; · 22 unmodified lines
23import * as ElectronTheme from "../../electron/ElectronTheme.ts";23import * as ElectronTheme from "../../electron/ElectronTheme.ts";
24import * as ElectronWindow from "../../electron/ElectronWindow.ts";24import * as ElectronWindow from "../../electron/ElectronWindow.ts";
25import * as IpcChannels from "../channels.ts";25import * as IpcChannels from "../channels.ts";
26import { makeIpcMethod, makeSyncIpcMethod } from "../DesktopIpc.ts";26import * as DesktopIpc from "../DesktopIpc.ts";
27import {27import {
28 extractDistroFromUncPath,28 extractDistroFromUncPath,
29 resolveWslPickFolderDefaultPath,29 resolveWslPickFolderDefaultPath,
@@ -46,7 +46,7 @@ function toWebSocketBaseUrl(httpBaseUrl: URL): string { · 16 unmodified lines
46 return url.href;46 return url.href;
47}47}
48 48
49export const getAppBranding = makeSyncIpcMethod({49export const getAppBranding = DesktopIpc.makeSyncIpcMethod({
50 channel: IpcChannels.GET_APP_BRANDING_CHANNEL,50 channel: IpcChannels.GET_APP_BRANDING_CHANNEL,
51 result: Schema.NullOr(DesktopAppBrandingSchema),51 result: Schema.NullOr(DesktopAppBrandingSchema),
52 handler: Effect.fn("desktop.ipc.window.getAppBranding")(function* () {52 handler: Effect.fn("desktop.ipc.window.getAppBranding")(function* () {
@@ -55,7 +55,7 @@ export const getAppBranding = makeSyncIpcMethod({ · 2 unmodified lines
55 }),55 }),
56});56});
57 57
58export const getLocalEnvironmentBootstraps = makeSyncIpcMethod({58export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
59 channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,59 channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
60 result: Schema.Array(DesktopEnvironmentBootstrapSchema),60 result: Schema.Array(DesktopEnvironmentBootstrapSchema),
61 handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBootstraps")(function* () {61 handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBootstraps")(function* () {
@@ -102,7 +102,7 @@ function extractWslDistroFromEnvironmentId(envId: string): string | null { · 40 unmodified lines
102 return suffix === "default" || suffix.length === 0 ? null : suffix;102 return suffix === "default" || suffix.length === 0 ? null : suffix;
103}103}
104 104
105export const getLocalEnvironmentBearerToken = makeIpcMethod({105export const getLocalEnvironmentBearerToken = DesktopIpc.makeIpcMethod({
106 channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL,106 channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL,
107 payload: Schema.Void,107 payload: Schema.Void,
108 result: Schema.String,108 result: Schema.String,
@@ -112,7 +112,7 @@ export const getLocalEnvironmentBearerToken = makeIpcMethod({ · 3 unmodified lines
112 }),112 }),
113});113});
114 114
115export const pickFolder = makeIpcMethod({115export const pickFolder = DesktopIpc.makeIpcMethod({
116 channel: IpcChannels.PICK_FOLDER_CHANNEL,116 channel: IpcChannels.PICK_FOLDER_CHANNEL,
117 payload: Schema.UndefinedOr(PickFolderOptionsSchema),117 payload: Schema.UndefinedOr(PickFolderOptionsSchema),
118 result: Schema.NullOr(Schema.String),118 result: Schema.NullOr(Schema.String),
@@ -179,7 +179,7 @@ export const pickFolder = makeIpcMethod({ · 60 unmodified lines
179 }),179 }),
180});180});
181 181
182export const confirm = makeIpcMethod({182export const confirm = DesktopIpc.makeIpcMethod({
183 channel: IpcChannels.CONFIRM_CHANNEL,183 channel: IpcChannels.CONFIRM_CHANNEL,
184 payload: Schema.String,184 payload: Schema.String,
185 result: Schema.Boolean,185 result: Schema.Boolean,
@@ -192,7 +192,7 @@ export const confirm = makeIpcMethod({ · 6 unmodified lines
192 }),192 }),
193});193});
194 194
195export const setTheme = makeIpcMethod({195export const setTheme = DesktopIpc.makeIpcMethod({
196 channel: IpcChannels.SET_THEME_CHANNEL,196 channel: IpcChannels.SET_THEME_CHANNEL,
197 payload: DesktopThemeSchema,197 payload: DesktopThemeSchema,
198 result: Schema.Void,198 result: Schema.Void,
@@ -202,7 +202,7 @@ export const setTheme = makeIpcMethod({ · 3 unmodified lines
202 }),202 }),
203});203});
204 204
205export const showContextMenu = makeIpcMethod({205export const showContextMenu = DesktopIpc.makeIpcMethod({
206 channel: IpcChannels.CONTEXT_MENU_CHANNEL,206 channel: IpcChannels.CONTEXT_MENU_CHANNEL,
207 payload: ContextMenuInput,207 payload: ContextMenuInput,
208 result: Schema.NullOr(Schema.String),208 result: Schema.NullOr(Schema.String),
@@ -223,7 +223,7 @@ export const showContextMenu = makeIpcMethod({ · 14 unmodified lines
223 }),223 }),
224});224});
225 225
226export const openExternal = makeIpcMethod({226export const openExternal = DesktopIpc.makeIpcMethod({
227 channel: IpcChannels.OPEN_EXTERNAL_CHANNEL,227 channel: IpcChannels.OPEN_EXTERNAL_CHANNEL,
228 payload: Schema.String,228 payload: Schema.String,
229 result: Schema.Boolean,229 result: Schema.Boolean,
apps/desktop/src/ipc/methods/wsl.test.ts+1−1
@@ -49,7 +49,7 @@ const unusedLifecycleRuntimeLayer = Layer.mergeAll( · 48 unmodified lines
49 DesktopState.layer,49 DesktopState.layer,
50 Layer.succeed(50 Layer.succeed(
51 DesktopEnvironment.DesktopEnvironment,51 DesktopEnvironment.DesktopEnvironment,
52 DesktopEnvironment.DesktopEnvironment.of({} as DesktopEnvironment.DesktopEnvironmentShape),52 DesktopEnvironment.DesktopEnvironment.of({} as DesktopEnvironment.DesktopEnvironment["Service"]),
53 ),53 ),
54 Layer.succeed(54 Layer.succeed(
55 DesktopWindow.DesktopWindow,55 DesktopWindow.DesktopWindow,
apps/desktop/src/main.ts+3−4
@@ -14,7 +14,6 @@ import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; · 13 unmodified lines
14import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";14import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";
15import serverPackageJson from "../../server/package.json" with { type: "json" };15import serverPackageJson from "../../server/package.json" with { type: "json" };
16 16
17import type { DesktopSettings as DesktopSettingsValue } from "./settings/DesktopAppSettings.ts";
18import * as DesktopIpc from "./ipc/DesktopIpc.ts";17import * as DesktopIpc from "./ipc/DesktopIpc.ts";
19import * as ElectronApp from "./electron/ElectronApp.ts";18import * as ElectronApp from "./electron/ElectronApp.ts";
20import * as ElectronDialog from "./electron/ElectronDialog.ts";19import * as ElectronDialog from "./electron/ElectronDialog.ts";
@@ -70,8 +69,8 @@ const desktopEnvironmentLayer = Layer.unwrap( · 49 unmodified lines
70);69);
71 70
72const resolveDesktopSshCliRunner = (71const resolveDesktopSshCliRunner = (
73 environment: DesktopEnvironment.DesktopEnvironmentShape,72 environment: DesktopEnvironment.DesktopEnvironment["Service"],
74 settings: DesktopSettingsValue,73 settings: DesktopAppSettings.DesktopSettings,
75): RemoteT3RunnerOptions => {74): RemoteT3RunnerOptions => {
76 const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath);75 const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath);
77 if (environment.isDevelopment && devRemoteEntryPath !== undefined) {76 if (environment.isDevelopment && devRemoteEntryPath !== undefined) {
@@ -112,7 +111,7 @@ const electronLayer = Layer.mergeAll( · 34 unmodified lines
112 ElectronTheme.layer,111 ElectronTheme.layer,
113 ElectronUpdater.layer,112 ElectronUpdater.layer,
114 ElectronWindow.layer,113 ElectronWindow.layer,
115 Layer.succeed(DesktopIpc.DesktopIpc, DesktopIpc.make(Electron.ipcMain)),114 DesktopIpc.layer(Electron.ipcMain),
116);115);
117 116
118const desktopFoundationLayer = Layer.mergeAll(117const desktopFoundationLayer = Layer.mergeAll(
apps/desktop/src/preview/Manager.test.ts+2−2
@@ -59,7 +59,7 @@ const environmentLayer = Layer.succeed( · 58 unmodified lines
59 DesktopEnvironment.DesktopEnvironment,59 DesktopEnvironment.DesktopEnvironment,
60 DesktopEnvironment.DesktopEnvironment.of({60 DesktopEnvironment.DesktopEnvironment.of({
61 browserArtifactsDir: "/tmp/t3/dev/browser-artifacts",61 browserArtifactsDir: "/tmp/t3/dev/browser-artifacts",
62 } as DesktopEnvironment.DesktopEnvironmentShape),62 } as DesktopEnvironment.DesktopEnvironment["Service"]),
63);63);
64 64
65const fileSystemLayer = FileSystem.layerNoop({65const fileSystemLayer = FileSystem.layerNoop({
@@ -82,7 +82,7 @@ const layer = PreviewManager.layer.pipe( · 16 unmodified lines
82 82
83const withManager = <A>(83const withManager = <A>(
84 use: (84 use: (
85 manager: PreviewManager.PreviewManagerShape,85 manager: PreviewManager.PreviewManager["Service"],
86 ) => Effect.Effect<A, PreviewManager.PreviewManagerError, Scope.Scope>,86 ) => Effect.Effect<A, PreviewManager.PreviewManagerError, Scope.Scope>,
87) =>87) =>
88 Effect.gen(function* () {88 Effect.gen(function* () {
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+1−1
@@ -64,7 +64,7 @@ function runShellEnvironment(input: { · 63 unmodified lines
64 DesktopEnvironment.DesktopEnvironment,64 DesktopEnvironment.DesktopEnvironment,
65 DesktopEnvironment.DesktopEnvironment.of({65 DesktopEnvironment.DesktopEnvironment.of({
66 platform: input.platform,66 platform: input.platform,
67 } as DesktopEnvironment.DesktopEnvironmentShape),67 } as DesktopEnvironment.DesktopEnvironment["Service"]),
68 );68 );
69 const spawnerLayer = Layer.succeed(69 const spawnerLayer = Layer.succeed(
70 ChildProcessSpawner.ChildProcessSpawner,70 ChildProcessSpawner.ChildProcessSpawner,
apps/desktop/src/updates/DesktopUpdates.ts+1−1
@@ -127,7 +127,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm · 126 unmodified lines
127function createBaseUpdateState(127function createBaseUpdateState(
128 channel: DesktopUpdateChannel,128 channel: DesktopUpdateChannel,
129 enabled: boolean,129 enabled: boolean,
130 environment: DesktopEnvironment.DesktopEnvironmentShape,130 environment: DesktopEnvironment.DesktopEnvironment["Service"],
131): DesktopUpdateState {131): DesktopUpdateState {
132 return {132 return {
133 ...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel),133 ...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel),
apps/desktop/src/window/DesktopWindow.test.ts+6−6
@@ -91,7 +91,7 @@ const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, { · 90 unmodified lines
91 png: Option.none<string>(),91 png: Option.none<string>(),
92 }),92 }),
93 resolveResourcePath: () => Effect.succeed(Option.none<string>()),93 resolveResourcePath: () => Effect.succeed(Option.none<string>()),
94} satisfies DesktopAssets.DesktopAssetsShape);94} satisfies DesktopAssets.DesktopAssets["Service"]);
95 95
96const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {96const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
97 getState: Effect.die("unexpected getState"),97 getState: Effect.die("unexpected getState"),
@@ -106,19 +106,19 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe · 8 unmodified lines
106 setMode: () => Effect.die("unexpected setMode"),106 setMode: () => Effect.die("unexpected setMode"),
107 setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),107 setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),
108 getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),108 getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),
109} satisfies DesktopServerExposure.DesktopServerExposureShape);109} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
110 110
111const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {111const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {
112 setApplicationMenu: () => Effect.void,112 setApplicationMenu: () => Effect.void,
113 popupTemplate: () => Effect.void,113 popupTemplate: () => Effect.void,
114 showContextMenu: () => Effect.succeed(Option.none()),114 showContextMenu: () => Effect.succeed(Option.none()),
115} satisfies ElectronMenu.ElectronMenuShape);115} satisfies ElectronMenu.ElectronMenu["Service"]);
116 116
117const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {117const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {
118 shouldUseDarkColors: Effect.succeed(false),118 shouldUseDarkColors: Effect.succeed(false),
119 setSource: () => Effect.void,119 setSource: () => Effect.void,
120 onUpdated: () => Effect.void,120 onUpdated: () => Effect.void,
121} satisfies ElectronTheme.ElectronThemeShape);121} satisfies ElectronTheme.ElectronTheme["Service"]);
122 122
123const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe(123const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe(
124 Layer.provide(124 Layer.provide(
@@ -156,7 +156,7 @@ function makeTestLayer(input: { · 31 unmodified lines
156 sendAll: () => Effect.void,156 sendAll: () => Effect.void,
157 destroyAll: Effect.void,157 destroyAll: Effect.void,
158 syncAllAppearance: (sync) => sync(input.window),158 syncAllAppearance: (sync) => sync(input.window),
159 } satisfies ElectronWindow.ElectronWindowShape);159 } satisfies ElectronWindow.ElectronWindow["Service"]);
160 160
161 return DesktopWindow.layer.pipe(161 return DesktopWindow.layer.pipe(
162 Layer.provide(162 Layer.provide(
@@ -172,7 +172,7 @@ function makeTestLayer(input: { · 9 unmodified lines
172 return true;172 return true;
173 }),173 }),
174 copyText: () => Effect.void,174 copyText: () => Effect.void,
175 } satisfies ElectronShell.ElectronShellShape),175 } satisfies ElectronShell.ElectronShell["Service"]),
176 electronThemeLayer,176 electronThemeLayer,
177 electronWindowLayer,177 electronWindowLayer,
178 Layer.mock(PreviewManager.PreviewManager)({178 Layer.mock(PreviewManager.PreviewManager)({
apps/mobile/.swiftlint.yml+1−0
@@ -1,5 +1,6 @@
1included:1included:
2 - ios/T3Code2 - ios/T3Code
3 - modules/t3-composer-editor/ios
3 - modules/t3-terminal/ios4 - modules/t3-terminal/ios
4 - modules/t3-review-diff/ios5 - modules/t3-review-diff/ios
5 6
apps/mobile/global.css+21−1
@@ -192,11 +192,31 @@ · 191 unmodified lines
192 }192 }
193}193}
194 194
195/* ─── Font family ───────────────────────────────────────────────────── */195/* ─── Typography ────────────────────────────────────────────────────── */
196@theme {196@theme {
197 --font-sans: "DMSans_400Regular";197 --font-sans: "DMSans_400Regular";
198 --font-medium: "DMSans_500Medium";198 --font-medium: "DMSans_500Medium";
199 --font-bold: "DMSans_700Bold";199 --font-bold: "DMSans_700Bold";
200
201 /* Keep this scale aligned with src/lib/typography.ts for native style props. */
202 --text-3xs: 10px;
203 --text-3xs--line-height: 13px;
204 --text-2xs: 11px;
205 --text-2xs--line-height: 15px;
206 --text-xs: 12px;
207 --text-xs--line-height: 16px;
208 --text-sm: 13px;
209 --text-sm--line-height: 18px;
210 --text-base: 15px;
211 --text-base--line-height: 22px;
212 --text-lg: 17px;
213 --text-lg--line-height: 22px;
214 --text-xl: 20px;
215 --text-xl--line-height: 26px;
216 --text-2xl: 24px;
217 --text-2xl--line-height: 30px;
218 --text-3xl: 28px;
219 --text-3xl--line-height: 34px;
200}220}
201 221
202/* ─── Custom utilities ──────────────────────────────────────────────── */222/* ─── Custom utilities ──────────────────────────────────────────────── */
apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift+27−4
@@ -275,8 +275,8 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { · 274 unmodified lines
275 fileTint: "#737373"275 fileTint: "#737373"
276 )276 )
277 private var fontFamily = "DMSans_400Regular"277 private var fontFamily = "DMSans_400Regular"
278 private var fontSize: CGFloat = 15278 private var fontSize: CGFloat = 14
279 private var lineHeight: CGFloat = 22279 private var lineHeight: CGFloat = 20
280 private var contentInsetVertical: CGFloat = 0280 private var contentInsetVertical: CGFloat = 0
281 private var shouldAutoFocus = false281 private var shouldAutoFocus = false
282 private var didAutoFocus = false282 private var didAutoFocus = false
@@ -460,9 +460,19 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { · 177 unmodified lines
460 guard !isApplyingControlledValue else {460 guard !isApplyingControlledValue else {
461 return461 return
462 }462 }
463 restoreBaseTypingAttributes()
463 emitSelection()464 emitSelection()
464 }465 }
465 466
467 public func textView(
468 _ textView: UITextView,
469 shouldChangeTextIn range: NSRange,
470 replacementText text: String
471 ) -> Bool {
472 restoreBaseTypingAttributes()
473 return true
474 }
475
466 public func textViewDidBeginEditing(_ textView: UITextView) {476 public func textViewDidBeginEditing(_ textView: UITextView) {
467 onComposerFocus()477 onComposerFocus()
468 }478 }
@@ -484,6 +494,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { · 15 unmodified lines
484 let targetSelection = requestedSelection ?? previousSelection494 let targetSelection = requestedSelection ?? previousSelection
485 requestedSelection = nil495 requestedSelection = nil
486 textView.selectedRange = displayRange(for: targetSelection)496 textView.selectedRange = displayRange(for: targetSelection)
497 restoreBaseTypingAttributes()
487 isApplyingControlledValue = false498 isApplyingControlledValue = false
488 updatePlaceholderVisibility()499 updatePlaceholderVisibility()
489 emitContentSizeIfNeeded()500 emitContentSizeIfNeeded()
@@ -556,7 +567,12 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { · 66 unmodified lines
556 size: image.size,567 size: image.size,
557 baselineOffset: baselineOffset568 baselineOffset: baselineOffset
558 )569 )
559 return NSAttributedString(attachment: attachment)570 let attributedAttachment = NSMutableAttributedString(attachment: attachment)
571 attributedAttachment.addAttributes(
572 baseAttributes(),
573 range: NSRange(location: 0, length: attributedAttachment.length)
574 )
575 return attributedAttachment
560 }576 }
561 577
562 private func renderChip(578 private func renderChip(
@@ -660,11 +676,18 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { · 97 unmodified lines
660 let font = UIFont(name: fontFamily, size: fontSize)676 let font = UIFont(name: fontFamily, size: fontSize)
661 ?? UIFont.systemFont(ofSize: fontSize)677 ?? UIFont.systemFont(ofSize: fontSize)
662 textView.font = font678 textView.font = font
663 textView.typingAttributes = baseAttributes()679 restoreBaseTypingAttributes()
664 placeholderLabel.font = font680 placeholderLabel.font = font
665 setNeedsLayout()681 setNeedsLayout()
666 }682 }
667 683
684 private func restoreBaseTypingAttributes() {
685 guard textView.markedTextRange == nil else {
686 return
687 }
688 textView.typingAttributes = baseAttributes()
689 }
690
668 private func applyTheme() {691 private func applyTheme() {
669 textView.textColor = UIColor(composerHex: theme.text) ?? .label692 textView.textColor = UIColor(composerHex: theme.text) ?? .label
670 placeholderLabel.textColor = UIColor(composerHex: theme.placeholder) ?? .placeholderText693 placeholderLabel.textColor = UIColor(composerHex: theme.placeholder) ?? .placeholderText
apps/mobile/src/app/+not-found.tsx+2−2
@@ -21,7 +21,7 @@ export default function NotFoundRoute() { · 20 unmodified lines
21 }}21 }}
22 style={[{ flex: 1 }, screenBgStyle]}22 style={[{ flex: 1 }, screenBgStyle]}
23 >23 >
24 <Text className="text-[28px] font-t3-bold text-foreground" selectable>24 <Text className="text-3xl font-t3-bold text-foreground" selectable>
25 Route not found25 Route not found
26 </Text>26 </Text>
27 <Link href="/" asChild>27 <Link href="/" asChild>
@@ -35,7 +35,7 @@ export default function NotFoundRoute() { · 7 unmodified lines
35 primaryBgStyle,35 primaryBgStyle,
36 ]}36 ]}
37 >37 >
38 <Text className="text-[16px] font-t3-bold text-primary-foreground">Return home</Text>38 <Text className="text-base font-t3-bold text-primary-foreground">Return home</Text>
39 </Pressable>39 </Pressable>
40 </Link>40 </Link>
41 </ScrollView>41 </ScrollView>
apps/mobile/src/app/connections/index.tsx+1−1
@@ -85,7 +85,7 @@ export default function ConnectionsRouteScreen() { · 84 unmodified lines
85 type="monochrome"85 type="monochrome"
86 />86 />
87 </View>87 </View>
88 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">88 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
89 No environments connected yet.{"\n"}Tap{" "}89 No environments connected yet.{"\n"}Tap{" "}
90 <Text className="font-t3-bold text-foreground">+</Text> to add one.90 <Text className="font-t3-bold text-foreground">+</Text> to add one.
91 </Text>91 </Text>
apps/mobile/src/app/connections/new.tsx+5−5
@@ -171,7 +171,7 @@ export default function ConnectionsNewRouteScreen() { · 170 unmodified lines
171 className="items-center gap-3 rounded-[24px] bg-card px-5 py-8"171 className="items-center gap-3 rounded-[24px] bg-card px-5 py-8"
172 style={{ borderCurve: "continuous" }}172 style={{ borderCurve: "continuous" }}
173 >173 >
174 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">174 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
175 Camera permission is required to scan a QR code.175 Camera permission is required to scan a QR code.
176 </Text>176 </Text>
177 <ConnectionSheetButton177 <ConnectionSheetButton
@@ -189,7 +189,7 @@ export default function ConnectionsNewRouteScreen() { · 11 unmodified lines
189 <View collapsable={false} className="gap-4 rounded-[24px] bg-card p-4">189 <View collapsable={false} className="gap-4 rounded-[24px] bg-card p-4">
190 <View collapsable={false} className="gap-1.5">190 <View collapsable={false} className="gap-1.5">
191 <Text191 <Text
192 className="text-[11px] font-t3-bold uppercase text-foreground-muted"192 className="text-2xs font-t3-bold uppercase text-foreground-muted"
193 style={{ letterSpacing: 0.8 }}193 style={{ letterSpacing: 0.8 }}
194 >194 >
195 Host195 Host
@@ -202,13 +202,13 @@ export default function ConnectionsNewRouteScreen() { · 6 unmodified lines
202 placeholderTextColor={placeholderColor}202 placeholderTextColor={placeholderColor}
203 value={hostInput}203 value={hostInput}
204 onChangeText={handleHostChange}204 onChangeText={handleHostChange}
205 className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"205 className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-base text-foreground"
206 />206 />
207 </View>207 </View>
208 208
209 <View collapsable={false} className="gap-1.5">209 <View collapsable={false} className="gap-1.5">
210 <Text210 <Text
211 className="text-[11px] font-t3-bold uppercase text-foreground-muted"211 className="text-2xs font-t3-bold uppercase text-foreground-muted"
212 style={{ letterSpacing: 0.8 }}212 style={{ letterSpacing: 0.8 }}
213 >213 >
214 Pairing code214 Pairing code
@@ -220,7 +220,7 @@ export default function ConnectionsNewRouteScreen() { · 5 unmodified lines
220 placeholderTextColor={placeholderColor}220 placeholderTextColor={placeholderColor}
221 value={codeInput}221 value={codeInput}
222 onChangeText={handleCodeChange}222 onChangeText={handleCodeChange}
223 className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"223 className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-base text-foreground"
224 />224 />
225 </View>225 </View>
226 226
apps/mobile/src/app/new/index.tsx+5−7
@@ -129,10 +129,10 @@ export default function NewTaskRoute() { · 128 unmodified lines
129 {items.length === 0 ? (129 {items.length === 0 ? (
130 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">130 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">
131 {projectEmptyState.loading ? <ActivityIndicator color={accentColor} /> : null}131 {projectEmptyState.loading ? <ActivityIndicator color={accentColor} /> : null}
132 <Text className="text-center text-[17px] font-t3-bold text-foreground">132 <Text className="text-center text-lg font-t3-bold text-foreground">
133 {projectEmptyState.title}133 {projectEmptyState.title}
134 </Text>134 </Text>
135 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">135 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
136 {projectEmptyState.detail}136 {projectEmptyState.detail}
137 </Text>137 </Text>
138 {!catalogState.hasReadyEnvironment ? (138 {!catalogState.hasReadyEnvironment ? (
@@ -140,7 +140,7 @@ export default function NewTaskRoute() { · 1 unmodified line
140 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"140 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"
141 onPress={() => router.push("/connections/new")}141 onPress={() => router.push("/connections/new")}
142 >142 >
143 <Text className="text-[13px] font-t3-bold text-primary-foreground">143 <Text className="text-sm font-t3-bold text-primary-foreground">
144 Add environment144 Add environment
145 </Text>145 </Text>
146 </Pressable>146 </Pressable>
@@ -149,7 +149,7 @@ export default function NewTaskRoute() { · 2 unmodified lines
149 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"149 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"
150 onPress={() => router.push("/new/add-project")}150 onPress={() => router.push("/new/add-project")}
151 >151 >
152 <Text className="text-[13px] font-t3-bold text-primary-foreground">152 <Text className="text-sm font-t3-bold text-primary-foreground">
153 Add new project153 Add new project
154 </Text>154 </Text>
155 </Pressable>155 </Pressable>
@@ -197,9 +197,7 @@ export default function NewTaskRoute() { · 41 unmodified lines
197 />197 />
198 </View>198 </View>
199 <View className="flex-1">199 <View className="flex-1">
200 <Text className="text-[16px] leading-[21px] font-t3-bold">200 <Text className="text-base leading-[21px] font-t3-bold">{item.title}</Text>
201 {item.title}
202 </Text>
203 </View>201 </View>
204 <SymbolView202 <SymbolView
205 name="chevron.right"203 name="chevron.right"
apps/mobile/src/app/settings/environments.tsx+11−11
@@ -111,7 +111,7 @@ export default function SettingsEnvironmentsRouteScreen() { · 110 unmodified lines
111 type="monochrome"111 type="monochrome"
112 />112 />
113 </View>113 </View>
114 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">114 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
115 No environments connected yet.{"\n"}Tap{" "}115 No environments connected yet.{"\n"}Tap{" "}
116 <Text className="font-t3-bold text-foreground">+</Text> to add one.116 <Text className="font-t3-bold text-foreground">+</Text> to add one.
117 </Text>117 </Text>
@@ -160,7 +160,7 @@ function ConfiguredCloudEnvironmentRows(props: { · 42 unmodified lines
160 return (160 return (
161 <View collapsable={false} className="mt-5 gap-3">161 <View collapsable={false} className="mt-5 gap-3">
162 <View className="flex-row items-center justify-between px-1">162 <View className="flex-row items-center justify-between px-1">
163 <Text className="text-[13px] font-t3-bold uppercase text-foreground-muted">T3 Cloud</Text>163 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Cloud</Text>
164 <Pressable164 <Pressable
165 accessibilityRole="button"165 accessibilityRole="button"
166 disabled={controller.relayDiscovery.isRefreshing}166 disabled={controller.relayDiscovery.isRefreshing}
@@ -204,16 +204,16 @@ function ConfiguredCloudEnvironmentRows(props: { · 37 unmodified lines
204 ) : controller.relayDiscovery.isRefreshing ? (204 ) : controller.relayDiscovery.isRefreshing ? (
205 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6">205 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6">
206 <ActivityIndicator color={iconColor} />206 <ActivityIndicator color={iconColor} />
207 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">207 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
208 Loading linked cloud environments.208 Loading linked cloud environments.
209 </Text>209 </Text>
210 </View>210 </View>
211 ) : controller.relayDiscovery.error ? (211 ) : controller.relayDiscovery.error ? (
212 <View collapsable={false} className="gap-3 rounded-[24px] bg-card p-5">212 <View collapsable={false} className="gap-3 rounded-[24px] bg-card p-5">
213 <Text className="text-[15px] font-t3-bold text-foreground">213 <Text className="text-base font-t3-bold text-foreground">
214 Could not load T3 Cloud environments214 Could not load T3 Cloud environments
215 </Text>215 </Text>
216 <Text className="text-[13px] leading-[18px] text-foreground-muted">216 <Text className="text-sm leading-[18px] text-foreground-muted">
217 {controller.relayDiscovery.error}217 {controller.relayDiscovery.error}
218 </Text>218 </Text>
219 {controller.relayDiscovery.errorTraceId ? (219 {controller.relayDiscovery.errorTraceId ? (
@@ -222,7 +222,7 @@ function ConfiguredCloudEnvironmentRows(props: { · 2 unmodified lines
222 </View>222 </View>
223 ) : (223 ) : (
224 <View collapsable={false} className="rounded-[24px] bg-card p-5">224 <View collapsable={false} className="rounded-[24px] bg-card p-5">
225 <Text className="text-[14px] leading-[20px] text-foreground-muted">225 <Text className="text-sm leading-[20px] text-foreground-muted">
226 No additional linked cloud environments.226 No additional linked cloud environments.
227 </Text>227 </Text>
228 </View>228 </View>
@@ -361,7 +361,7 @@ function CloudEnvironmentRowShell(props: { · 132 unmodified lines
361 <View className="min-w-0 flex-row items-center gap-2">361 <View className="min-w-0 flex-row items-center gap-2">
362 <ConnectionStatusDot state={props.connectionState} pulse={shouldPulse} size={7} />362 <ConnectionStatusDot state={props.connectionState} pulse={shouldPulse} size={7} />
363 <Text363 <Text
364 className="min-w-0 flex-shrink text-[16px] font-t3-bold leading-[21px] text-foreground"364 className="min-w-0 flex-shrink text-base font-t3-bold leading-[21px] text-foreground"
365 numberOfLines={1}365 numberOfLines={1}
366 >366 >
367 {props.label}367 {props.label}
@@ -370,7 +370,7 @@ function CloudEnvironmentRowShell(props: { · 2 unmodified lines
370 {props.connectionError ? (370 {props.connectionError ? (
371 <Text371 <Text
372 aria-hidden372 aria-hidden
373 className={cn("absolute left-0 right-0 text-[12px] leading-[16px]", statusClassName)}373 className={cn("absolute left-0 right-0 text-xs leading-[16px]", statusClassName)}
374 onTextLayout={onMeasuredErrorTextLayout}374 onTextLayout={onMeasuredErrorTextLayout}
375 style={{ opacity: 0, zIndex: -1 }}375 style={{ opacity: 0, zIndex: -1 }}
376 >376 >
@@ -384,7 +384,7 @@ function CloudEnvironmentRowShell(props: { · 7 unmodified lines
384 className="min-w-0 flex-row items-start gap-1"384 className="min-w-0 flex-row items-start gap-1"
385 >385 >
386 <Text386 <Text
387 className={cn("min-w-0 flex-1 text-[12px] leading-[16px]", statusClassName)}387 className={cn("min-w-0 flex-1 text-xs leading-[16px]", statusClassName)}
388 numberOfLines={isErrorExpanded ? undefined : 1}388 numberOfLines={isErrorExpanded ? undefined : 1}
389 >389 >
390 {statusText}390 {statusText}
@@ -394,7 +394,7 @@ function CloudEnvironmentRowShell(props: { · 3 unmodified lines
394 <Text394 <Text
395 accessibilityHint="Copies the trace ID"395 accessibilityHint="Copies the trace ID"
396 accessibilityRole="button"396 accessibilityRole="button"
397 className={cn("text-[12px] leading-[16px] underline", statusClassName)}397 className={cn("text-xs leading-[16px] underline", statusClassName)}
398 onLongPress={(event) => {398 onLongPress={(event) => {
399 event.stopPropagation();399 event.stopPropagation();
400 copyTextWithHaptic(errorTraceId);400 copyTextWithHaptic(errorTraceId);
@@ -446,7 +446,7 @@ function CopyTraceIdButton(props: { readonly traceId: string }) { · 45 unmodified lines
446 className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70"446 className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70"
447 >447 >
448 <SymbolView name="doc.on.doc" size={12} tintColor={iconColor} type="monochrome" />448 <SymbolView name="doc.on.doc" size={12} tintColor={iconColor} type="monochrome" />
449 <Text className="text-[12px] font-t3-bold text-foreground">Copy trace ID</Text>449 <Text className="text-xs font-t3-bold text-foreground">Copy trace ID</Text>
450 </Pressable>450 </Pressable>
451 );451 );
452}452}
apps/mobile/src/app/settings/index.tsx+7−7
@@ -346,7 +346,7 @@ function ConfiguredSettingsRouteScreen() { · 345 unmodified lines
346 onPress={openAccount}346 onPress={openAccount}
347 />347 />
348 </SettingsSection>348 </SettingsSection>
349 <Text className="px-2 text-[13px] leading-[18px] text-foreground-muted">349 <Text className="px-2 text-sm leading-[18px] text-foreground-muted">
350 T3 Code works locally without signing in. Cloud features are optional.350 T3 Code works locally without signing in. Cloud features are optional.
351 </Text>351 </Text>
352 </View>352 </View>
@@ -389,7 +389,7 @@ type SymbolName = ComponentProps<typeof SymbolView>["name"]; · 36 unmodified lines
389function SettingsSection(props: { readonly title: string; readonly children: ReactNode }) {389function SettingsSection(props: { readonly title: string; readonly children: ReactNode }) {
390 return (390 return (
391 <View className="gap-2">391 <View className="gap-2">
392 <Text className="px-2 text-[13px] font-t3-medium text-foreground-muted">{props.title}</Text>392 <Text className="px-2 text-sm font-t3-medium text-foreground-muted">{props.title}</Text>
393 <View393 <View
394 className="overflow-hidden rounded-[28px] bg-card"394 className="overflow-hidden rounded-[28px] bg-card"
395 style={{ borderCurve: "continuous" }}395 style={{ borderCurve: "continuous" }}
@@ -413,8 +413,8 @@ function AppSettingsSection() { · 17 unmodified lines
413 type="monochrome"413 type="monochrome"
414 weight="regular"414 weight="regular"
415 />415 />
416 <Text className="flex-1 text-[17px] text-foreground">Version</Text>416 <Text className="flex-1 text-lg text-foreground">Version</Text>
417 <Text className="text-[17px] text-foreground-muted">Alpha</Text>417 <Text className="text-lg text-foreground-muted">Alpha</Text>
418 </View>418 </View>
419 </SettingsSection>419 </SettingsSection>
420 );420 );
@@ -444,13 +444,13 @@ function SettingsRow(props: { · 23 unmodified lines
444 style={{ opacity: props.disabled ? 0.45 : 1 }}444 style={{ opacity: props.disabled ? 0.45 : 1 }}
445 >445 >
446 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />446 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />
447 <Text className="shrink-0 text-[17px] text-foreground" numberOfLines={1}>447 <Text className="shrink-0 text-lg text-foreground" numberOfLines={1}>
448 {props.label}448 {props.label}
449 </Text>449 </Text>
450 <View className="min-w-0 flex-1 items-end">450 <View className="min-w-0 flex-1 items-end">
451 {props.value ? (451 {props.value ? (
452 <Text452 <Text
453 className="max-w-[180px] text-right text-[16px] text-foreground-muted"453 className="max-w-[180px] text-right text-base text-foreground-muted"
454 ellipsizeMode="middle"454 ellipsizeMode="middle"
455 numberOfLines={1}455 numberOfLines={1}
456 >456 >
@@ -502,7 +502,7 @@ function SettingsSwitchRow(props: { · 45 unmodified lines
502 style={{ opacity: props.disabled ? 0.45 : 1 }}502 style={{ opacity: props.disabled ? 0.45 : 1 }}
503 >503 >
504 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />504 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />
505 <Text className="flex-1 text-[17px] text-foreground">{props.label}</Text>505 <Text className="flex-1 text-lg text-foreground">{props.label}</Text>
506 <Switch506 <Switch
507 disabled={props.disabled}507 disabled={props.disabled}
508 ios_backgroundColor={track}508 ios_backgroundColor={track}
apps/mobile/src/components/AppText.tsx+1−1
@@ -40,7 +40,7 @@ export function AppTextInput({ · 39 unmodified lines
40 <RNTextInput40 <RNTextInput
41 ref={ref}41 ref={ref}
42 className={cn(42 className={cn(
43 "min-h-[54px] rounded-2xl border border-input-border bg-input px-3.5 py-3 font-sans text-[15px] text-foreground",43 "min-h-[54px] rounded-2xl border border-input-border bg-input px-3.5 py-3 font-sans text-base text-foreground",
44 className,44 className,
45 )}45 )}
46 placeholderTextColor={placeholderTextColor ?? placeholderColor}46 placeholderTextColor={placeholderTextColor ?? placeholderColor}
apps/mobile/src/components/BrandMark.tsx+3−6
@@ -22,15 +22,12 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab · 21 unmodified lines
22 />22 />
23 <View className="gap-1">23 <View className="gap-1">
24 <View className="flex-row items-center gap-2">24 <View className="flex-row items-center gap-2">
25 <Text25 <Text className="text-lg font-t3-bold text-foreground" style={{ letterSpacing: -0.4 }}>
26 className="text-[17px] font-t3-bold text-foreground"
27 style={{ letterSpacing: -0.4 }}
28 >
29 T3 Code26 T3 Code
30 </Text>27 </Text>
31 <View className="rounded-full bg-subtle px-2 py-1">28 <View className="rounded-full bg-subtle px-2 py-1">
32 <Text29 <Text
33 className="text-[10px] font-t3-bold uppercase text-foreground-muted"30 className="text-3xs font-t3-bold uppercase text-foreground-muted"
34 style={{ letterSpacing: 1.1 }}31 style={{ letterSpacing: 1.1 }}
35 >32 >
36 {stageLabel}33 {stageLabel}
@@ -38,7 +35,7 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab · 1 unmodified line
38 </View>35 </View>
39 </View>36 </View>
40 {!compact ? (37 {!compact ? (
41 <Text className="text-[12px] font-medium text-foreground-muted">38 <Text className="text-xs font-medium text-foreground-muted">
42 Mobile control surface for your live coding environments39 Mobile control surface for your live coding environments
43 </Text>40 </Text>
44 ) : null}41 ) : null}
apps/mobile/src/components/ComposerToolbarTrigger.tsx+1−1
@@ -223,7 +223,7 @@ export function ComposerToolbarButton(props: { · 222 unmodified lines
223 {props.label ? (223 {props.label ? (
224 <Text224 <Text
225 className={cn(225 className={cn(
226 "shrink text-center text-[13px] font-t3-bold",226 "shrink text-center text-sm font-t3-bold",
227 variant === "primary"227 variant === "primary"
228 ? props.disabled228 ? props.disabled
229 ? "text-foreground-muted"229 ? "text-foreground-muted"
apps/mobile/src/components/ControlPill.tsx+1−1
@@ -48,7 +48,7 @@ export function ControlPill(props: { · 47 unmodified lines
48 : "bg-subtle",48 : "bg-subtle",
49 );49 );
50 const labelClassName = cn(50 const labelClassName = cn(
51 "text-center text-[12px] font-t3-bold",51 "text-center text-xs font-t3-bold",
52 variant === "primary"52 variant === "primary"
53 ? props.disabled53 ? props.disabled
54 ? "text-foreground-muted"54 ? "text-foreground-muted"
apps/mobile/src/components/EmptyState.tsx+1−3
@@ -19,9 +19,7 @@ export function EmptyState(props: { · 18 unmodified lines
19 className="mt-4 self-start rounded-full bg-primary px-4 py-2.5 active:opacity-70"19 className="mt-4 self-start rounded-full bg-primary px-4 py-2.5 active:opacity-70"
20 onPress={props.onAction}20 onPress={props.onAction}
21 >21 >
22 <Text className="text-[13px] font-t3-bold text-primary-foreground">22 <Text className="text-sm font-t3-bold text-primary-foreground">{props.actionLabel}</Text>
23 {props.actionLabel}
24 </Text>
25 </Pressable>23 </Pressable>
26 ) : null}24 ) : null}
27 </View>25 </View>
apps/mobile/src/components/ErrorBanner.tsx+1−1
@@ -4,7 +4,7 @@ import { AppText as Text } from "./AppText"; · 3 unmodified lines
4export function ErrorBanner(props: { readonly message: string }) {4export function ErrorBanner(props: { readonly message: string }) {
5 return (5 return (
6 <View className="rounded-2xl border border-rose-300/70 bg-rose-100/80 px-3.5 py-3 dark:border-rose-400/28 dark:bg-rose-500/12">6 <View className="rounded-2xl border border-rose-300/70 bg-rose-100/80 px-3.5 py-3 dark:border-rose-400/28 dark:bg-rose-500/12">
7 <Text className="font-t3-medium text-[13px] leading-[18px] text-rose-700 dark:text-rose-300">7 <Text className="font-t3-medium text-sm leading-[18px] text-rose-700 dark:text-rose-300">
8 {props.message}8 {props.message}
9 </Text>9 </Text>
10 </View>10 </View>
apps/mobile/src/components/StatusPill.tsx+1−1
@@ -26,7 +26,7 @@ export function StatusPill( · 25 unmodified lines
26 <Text26 <Text
27 className={cn(27 className={cn(
28 "font-t3-bold",28 "font-t3-bold",
29 size === "compact" ? "text-[11px]" : "text-xs",29 size === "compact" ? "text-2xs" : "text-xs",
30 props.textClassName,30 props.textClassName,
31 )}31 )}
32 >32 >
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+11−9
@@ -147,14 +147,14 @@ function ProjectGroupLabel(props: { · 146 unmodified lines
147 workspaceRoot={props.project.workspaceRoot}147 workspaceRoot={props.project.workspaceRoot}
148 />148 />
149 <Text149 <Text
150 className="flex-1 text-[12px] font-t3-medium uppercase text-foreground-muted"150 className="flex-1 text-xs font-t3-medium uppercase text-foreground-muted"
151 numberOfLines={1}151 numberOfLines={1}
152 style={{ letterSpacing: 0.5 }}152 style={{ letterSpacing: 0.5 }}
153 >153 >
154 {props.project.title}154 {props.project.title}
155 </Text>155 </Text>
156 {props.environmentLabel ? (156 {props.environmentLabel ? (
157 <Text className="max-w-[42%] text-[11px] text-foreground-tertiary" numberOfLines={1}>157 <Text className="max-w-[42%] text-2xs text-foreground-tertiary" numberOfLines={1}>
158 {props.environmentLabel}158 {props.environmentLabel}
159 </Text>159 </Text>
160 ) : null}160 ) : null}
@@ -265,13 +265,13 @@ function ArchivedThreadRow(props: { · 104 unmodified lines
265 <View className="min-w-0 flex-1 gap-1">265 <View className="min-w-0 flex-1 gap-1">
266 <View className="flex-row items-center gap-2">266 <View className="flex-row items-center gap-2">
267 <Text267 <Text
268 className="min-w-0 flex-1 text-[15px] font-t3-bold leading-[20px] text-foreground"268 className="min-w-0 flex-1 text-base font-t3-bold leading-[20px] text-foreground"
269 numberOfLines={1}269 numberOfLines={1}
270 >270 >
271 {props.thread.title}271 {props.thread.title}
272 </Text>272 </Text>
273 <Text273 <Text
274 className="text-[12px] text-foreground-tertiary"274 className="text-xs text-foreground-tertiary"
275 style={{ fontVariant: ["tabular-nums"] }}275 style={{ fontVariant: ["tabular-nums"] }}
276 >276 >
277 {timestamp}277 {timestamp}
@@ -286,7 +286,7 @@ function ArchivedThreadRow(props: { · 8 unmodified lines
286 type="monochrome"286 type="monochrome"
287 />287 />
288 <Text288 <Text
289 className="min-w-0 flex-1 text-[11px] text-foreground-tertiary"289 className="min-w-0 flex-1 text-2xs text-foreground-tertiary"
290 numberOfLines={1}290 numberOfLines={1}
291 style={{ fontFamily: "monospace" }}291 style={{ fontFamily: "monospace" }}
292 >292 >
@@ -314,10 +314,12 @@ function ArchivedThreadRow(props: { · 21 unmodified lines
314function ArchiveError(props: { readonly message: string; readonly onRetry: () => void }) {314function ArchiveError(props: { readonly message: string; readonly onRetry: () => void }) {
315 return (315 return (
316 <View className="rounded-[20px] border border-danger-border bg-danger p-4">316 <View className="rounded-[20px] border border-danger-border bg-danger p-4">
317 <Text className="font-t3-bold text-danger-foreground">Could not load every archive</Text>317 <Text className="text-base font-t3-bold text-danger-foreground">
318 <Text className="mt-1 text-[13px] leading-[18px] text-foreground-muted">{props.message}</Text>318 Could not load every archive
319 </Text>
320 <Text className="mt-1 text-sm leading-[18px] text-foreground-muted">{props.message}</Text>
319 <Pressable className="mt-3 self-start active:opacity-60" onPress={props.onRetry}>321 <Pressable className="mt-3 self-start active:opacity-60" onPress={props.onRetry}>
320 <Text className="text-[13px] font-t3-bold text-danger-foreground">Try again</Text>322 <Text className="text-sm font-t3-bold text-danger-foreground">Try again</Text>
321 </Pressable>323 </Pressable>
322 </View>324 </View>
323 );325 );
@@ -386,7 +388,7 @@ export function ArchivedThreadsScreen(props: { · 62 unmodified lines
386 {isInitialLoad ? (388 {isInitialLoad ? (
387 <View className="items-center py-16">389 <View className="items-center py-16">
388 <ActivityIndicator color={refreshTint} />390 <ActivityIndicator color={refreshTint} />
389 <Text className="mt-3 text-[13px] text-foreground-muted">Loading archive…</Text>391 <Text className="mt-3 text-sm text-foreground-muted">Loading archive…</Text>
390 </View>392 </View>
391 ) : props.groups.length === 0 ? (393 ) : props.groups.length === 0 ? (
392 <EmptyState394 <EmptyState
apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx+8−12
@@ -2,6 +2,7 @@ import { useWaitlist } from "@clerk/expo"; · 1 unmodified line
2import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native";2import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
3import { useState } from "react";3import { useState } from "react";
4 4
5import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
5import { useThemeColor } from "../../lib/useThemeColor";6import { useThemeColor } from "../../lib/useThemeColor";
6 7
7export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) {8export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) {
@@ -141,12 +142,11 @@ function useCloudWaitlistColors() { · 133 unmodified lines
141const styles = StyleSheet.create({142const styles = StyleSheet.create({
142 body: {143 body: {
143 fontFamily: "DMSans_400Regular",144 fontFamily: "DMSans_400Regular",
144 fontSize: 15,145 ...MOBILE_TYPOGRAPHY.body,
145 lineHeight: 21,
146 },146 },
147 buttonText: {147 buttonText: {
148 fontFamily: "DMSans_700Bold",148 fontFamily: "DMSans_700Bold",
149 fontSize: 16,149 fontSize: MOBILE_TYPOGRAPHY.body.fontSize,
150 },150 },
151 content: {151 content: {
152 gap: 18,152 gap: 18,
@@ -156,8 +156,7 @@ const styles = StyleSheet.create({ · 3 unmodified lines
156 },156 },
157 error: {157 error: {
158 fontFamily: "DMSans_400Regular",158 fontFamily: "DMSans_400Regular",
159 fontSize: 13,159 ...MOBILE_TYPOGRAPHY.footnote,
160 lineHeight: 18,
161 },160 },
162 field: {161 field: {
163 gap: 8,162 gap: 8,
@@ -167,15 +166,14 @@ const styles = StyleSheet.create({ · 3 unmodified lines
167 borderRadius: 16,166 borderRadius: 16,
168 borderWidth: 1,167 borderWidth: 1,
169 fontFamily: "DMSans_400Regular",168 fontFamily: "DMSans_400Regular",
170 fontSize: 17,169 fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
171 minHeight: 54,170 minHeight: 54,
172 paddingHorizontal: 16,171 paddingHorizontal: 16,
173 paddingVertical: 14,172 paddingVertical: 14,
174 },173 },
175 label: {174 label: {
176 fontFamily: "DMSans_700Bold",175 fontFamily: "DMSans_700Bold",
177 fontSize: 13,176 ...MOBILE_TYPOGRAPHY.footnote,
178 lineHeight: 18,
179 },177 },
180 primaryButton: {178 primaryButton: {
181 alignItems: "center",179 alignItems: "center",
@@ -196,13 +194,11 @@ const styles = StyleSheet.create({ · 14 unmodified lines
196 },194 },
197 signInText: {195 signInText: {
198 fontFamily: "DMSans_700Bold",196 fontFamily: "DMSans_700Bold",
199 fontSize: 15,197 ...MOBILE_TYPOGRAPHY.body,
200 lineHeight: 21,
201 },198 },
202 title: {199 title: {
203 fontFamily: "DMSans_700Bold",200 fontFamily: "DMSans_700Bold",
204 fontSize: 20,201 ...MOBILE_TYPOGRAPHY.title,
205 lineHeight: 26,
206 textAlign: "center",202 textAlign: "center",
207 },203 },
208});204});
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+9−12
@@ -76,19 +76,16 @@ export function ConnectionEnvironmentRow(props: { · 75 unmodified lines
76 />76 />
77 77
78 <View className="flex-1 gap-0.5">78 <View className="flex-1 gap-0.5">
79 <Text79 <Text className="text-base font-t3-bold leading-[21px] text-foreground" numberOfLines={1}>
80 className="text-[16px] font-t3-bold leading-[21px] text-foreground"
81 numberOfLines={1}
82 >
83 {props.environment.environmentLabel}80 {props.environment.environmentLabel}
84 </Text>81 </Text>
85 <Text className="text-[12px] leading-[16px] text-foreground-muted" numberOfLines={1}>82 <Text className="text-xs leading-[16px] text-foreground-muted" numberOfLines={1}>
86 {props.environment.displayUrl}83 {props.environment.displayUrl}
87 </Text>84 </Text>
88 {statusLabel ? (85 {statusLabel ? (
89 <Text86 <Text
90 className={cn(87 className={cn(
91 "text-[12px] leading-[16px]",88 "text-xs leading-[16px]",
92 hasConnectionFailure ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted",89 hasConnectionFailure ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted",
93 )}90 )}
94 numberOfLines={props.expanded ? undefined : 1}91 numberOfLines={props.expanded ? undefined : 1}
@@ -137,14 +134,14 @@ export function ConnectionEnvironmentRow(props: { · 42 unmodified lines
137 className="gap-3 px-4 pb-4"134 className="gap-3 px-4 pb-4"
138 >135 >
139 {props.environment.isRelayManaged ? (136 {props.environment.isRelayManaged ? (
140 <Text className="text-[13px] leading-[18px] text-foreground-muted">137 <Text className="text-sm leading-[18px] text-foreground-muted">
141 Managed by T3 Cloud. Tunnel details update automatically.138 Managed by T3 Cloud. Tunnel details update automatically.
142 </Text>139 </Text>
143 ) : (140 ) : (
144 <>141 <>
145 <View className="gap-1.5">142 <View className="gap-1.5">
146 <Text143 <Text
147 className="text-[11px] font-t3-bold uppercase text-foreground-muted"144 className="text-2xs font-t3-bold uppercase text-foreground-muted"
148 style={{ letterSpacing: 0.8 }}145 style={{ letterSpacing: 0.8 }}
149 >146 >
150 Label147 Label
@@ -156,13 +153,13 @@ export function ConnectionEnvironmentRow(props: { · 5 unmodified lines
156 placeholderTextColor={placeholderColor}153 placeholderTextColor={placeholderColor}
157 value={label}154 value={label}
158 onChangeText={setLabel}155 onChangeText={setLabel}
159 className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-[15px] text-foreground"156 className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-base text-foreground"
160 />157 />
161 </View>158 </View>
162 159
163 <View className="gap-1.5">160 <View className="gap-1.5">
164 <Text161 <Text
165 className="text-[11px] font-t3-bold uppercase text-foreground-muted"162 className="text-2xs font-t3-bold uppercase text-foreground-muted"
166 style={{ letterSpacing: 0.8 }}163 style={{ letterSpacing: 0.8 }}
167 >164 >
168 URL165 URL
@@ -175,7 +172,7 @@ export function ConnectionEnvironmentRow(props: { · 6 unmodified lines
175 placeholderTextColor={placeholderColor}172 placeholderTextColor={placeholderColor}
176 value={url}173 value={url}
177 onChangeText={setUrl}174 onChangeText={setUrl}
178 className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-[15px] text-foreground"175 className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-base text-foreground"
179 />176 />
180 </View>177 </View>
181 </>178 </>
@@ -189,7 +186,7 @@ export function ConnectionEnvironmentRow(props: { · 7 unmodified lines
189 >186 >
190 <SymbolView name="checkmark" size={13} tintColor={primaryFg} type="monochrome" />187 <SymbolView name="checkmark" size={13} tintColor={primaryFg} type="monochrome" />
191 <Text188 <Text
192 className="text-[12px] font-t3-bold uppercase text-primary-foreground"189 className="text-xs font-t3-bold uppercase text-primary-foreground"
193 style={{ letterSpacing: 0.8 }}190 style={{ letterSpacing: 0.8 }}
194 >191 >
195 Save192 Save
apps/mobile/src/features/connection/ConnectionSheetButton.tsx+1−1
@@ -104,7 +104,7 @@ export function ConnectionSheetButton(props: { · 103 unmodified lines
104 type="monochrome"104 type="monochrome"
105 />105 />
106 <Text106 <Text
107 className="text-[12px] font-t3-bold uppercase"107 className="text-xs font-t3-bold uppercase"
108 style={{ color: colors.textColor, letterSpacing: 0.8 }}108 style={{ color: colors.textColor, letterSpacing: 0.8 }}
109 >109 >
110 {props.label}110 {props.label}
apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx+3−3
@@ -73,10 +73,10 @@ export function EnvironmentConnectionNotice(props: { · 72 unmodified lines
73 />73 />
74 )}74 )}
75 75
76 <Text className="text-center text-[17px] font-t3-bold leading-[22px] text-foreground">76 <Text className="text-center text-lg font-t3-bold leading-[22px] text-foreground">
77 {noticeTitle(props.connection.phase, props.environmentLabel)}77 {noticeTitle(props.connection.phase, props.environmentLabel)}
78 </Text>78 </Text>
79 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">79 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
80 {noticeDetail(props.connection.phase, props.resourceName, props.connection.error)}80 {noticeDetail(props.connection.phase, props.resourceName, props.connection.error)}
81 {props.connection.traceId ? (81 {props.connection.traceId ? (
82 <>82 <>
@@ -99,7 +99,7 @@ export function EnvironmentConnectionNotice(props: { · 16 unmodified lines
99 className="mt-1 rounded-full bg-subtle px-4 py-2.5 active:opacity-70"99 className="mt-1 rounded-full bg-subtle px-4 py-2.5 active:opacity-70"
100 onPress={props.onRetry}100 onPress={props.onRetry}
101 >101 >
102 <Text className="text-[13px] font-t3-bold text-foreground">Retry now</Text>102 <Text className="text-sm font-t3-bold text-foreground">Retry now</Text>
103 </Pressable>103 </Pressable>
104 ) : null}104 ) : null}
105 </View>105 </View>
apps/mobile/src/features/files/FileMarkdownPreview.tsx+3−4
@@ -7,6 +7,7 @@ import { · 6 unmodified lines
7} from "react-native-nitro-markdown";7} from "react-native-nitro-markdown";
8import { Linking, ScrollView, Text as NativeText, View } from "react-native";8import { Linking, ScrollView, Text as NativeText, View } from "react-native";
9 9
10import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
10import { useThemeColor } from "../../lib/useThemeColor";11import { useThemeColor } from "../../lib/useThemeColor";
11import {12import {
12 hasNativeSelectableMarkdownText,13 hasNativeSelectableMarkdownText,
@@ -72,8 +73,7 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { · 59 unmodified lines
72 text: {73 text: {
73 color: body,74 color: body,
74 fontFamily: "DMSans_400Regular",75 fontFamily: "DMSans_400Regular",
75 fontSize: 15,76 ...MOBILE_TYPOGRAPHY.body,
76 lineHeight: 22,
77 },77 },
78 heading: {78 heading: {
79 color: strong,79 color: strong,
@@ -123,8 +123,7 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { · 43 unmodified lines
123 skillTextColor: codeText,123 skillTextColor: codeText,
124 quoteMarkerColor: blockquoteBorder,124 quoteMarkerColor: blockquoteBorder,
125 dividerColor: horizontalRule,125 dividerColor: horizontalRule,
126 fontSize: 15,126 ...MOBILE_TYPOGRAPHY.body,
127 lineHeight: 22,
128 fontFamily: "DMSans_400Regular",127 fontFamily: "DMSans_400Regular",
129 headingFontFamily: "DMSans_700Bold",128 headingFontFamily: "DMSans_700Bold",
130 boldFontFamily: "DMSans_700Bold",129 boldFontFamily: "DMSans_700Bold",
apps/mobile/src/features/files/FileTreeBrowser.tsx+6−8
@@ -64,7 +64,7 @@ const FileTreeRow = memo(function FileTreeRow(props: { · 63 unmodified lines
64 <PierreEntryIcon path={node.path} kind={node.kind} size={17} />64 <PierreEntryIcon path={node.path} kind={node.kind} size={17} />
65 <Text65 <Text
66 className={cn(66 className={cn(
67 "min-w-0 flex-1 text-[14px] leading-[19px]",67 "min-w-0 flex-1 text-sm leading-[19px]",
68 selected ? "font-t3-bold text-foreground" : "font-t3-medium text-foreground-secondary",68 selected ? "font-t3-bold text-foreground" : "font-t3-medium text-foreground-secondary",
69 )}69 )}
70 numberOfLines={1}70 numberOfLines={1}
@@ -72,7 +72,7 @@ const FileTreeRow = memo(function FileTreeRow(props: { · 1 unmodified line
72 {node.name}72 {node.name}
73 </Text>73 </Text>
74 {node.kind === "directory" ? (74 {node.kind === "directory" ? (
75 <Text className="text-[11px] font-t3-medium text-foreground-tertiary">75 <Text className="text-2xs font-t3-medium text-foreground-tertiary">
76 {node.children.length}76 {node.children.length}
77 </Text>77 </Text>
78 ) : null}78 ) : null}
@@ -142,10 +142,8 @@ export function FileTreeBrowser(props: { · 63 unmodified lines
142 <View className="flex-1 bg-sheet">142 <View className="flex-1 bg-sheet">
143 {props.error && props.entries.length === 0 ? (143 {props.error && props.entries.length === 0 ? (
144 <View className="px-4 py-5">144 <View className="px-4 py-5">
145 <Text className="text-[13px] font-t3-bold text-foreground">Files unavailable</Text>145 <Text className="text-sm font-t3-bold text-foreground">Files unavailable</Text>
146 <Text className="mt-1 text-[12px] leading-[18px] text-foreground-muted">146 <Text className="mt-1 text-xs leading-[18px] text-foreground-muted">{props.error}</Text>
147 {props.error}
148 </Text>
149 </View>147 </View>
150 ) : (148 ) : (
151 <FlatList149 <FlatList
@@ -174,8 +172,8 @@ export function FileTreeBrowser(props: { · 22 unmodified lines
174 <ActivityIndicator size="small" />172 <ActivityIndicator size="small" />
175 ) : (173 ) : (
176 <>174 <>
177 <Text className="text-[13px] font-t3-bold text-foreground">No files found</Text>175 <Text className="text-sm font-t3-bold text-foreground">No files found</Text>
178 <Text className="mt-1 text-[12px] leading-[18px] text-foreground-muted">176 <Text className="mt-1 text-xs leading-[18px] text-foreground-muted">
179 {props.searchQuery.trim().length > 0177 {props.searchQuery.trim().length > 0
180 ? "Try a different search."178 ? "Try a different search."
181 : "The workspace file index is empty."}179 : "The workspace file index is empty."}
apps/mobile/src/features/files/SourceFileSurface.tsx+15−9
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
18} from "../review/reviewDiffRendering";18} from "../review/reviewDiffRendering";
19import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter";19import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter";
20import { cn } from "../../lib/cn";20import { cn } from "../../lib/cn";
21import { MOBILE_CODE_SURFACE } from "../../lib/typography";
21import {22import {
22 buildNativeSourceRows,23 buildNativeSourceRows,
23 buildNativeSourceTokens,24 buildNativeSourceTokens,
@@ -28,8 +29,8 @@ import { · 4 unmodified lines
28} from "./nativeSourceFileAdapter";29} from "./nativeSourceFileAdapter";
29import { sourceHighlightAtom } from "./sourceHighlightingState";30import { sourceHighlightAtom } from "./sourceHighlightingState";
30 31
31const SOURCE_LINE_HEIGHT = 24;32const SOURCE_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight;
32const SOURCE_LINE_NUMBER_WIDTH = 58;33const SOURCE_LINE_NUMBER_WIDTH = MOBILE_CODE_SURFACE.gutterWidth;
33const NATIVE_SOURCE_STYLE_JSON = JSON.stringify(NATIVE_SOURCE_STYLE);34const NATIVE_SOURCE_STYLE_JSON = JSON.stringify(NATIVE_SOURCE_STYLE);
34 35
35interface SourceFileSurfaceProps {36interface SourceFileSurfaceProps {
@@ -56,10 +57,12 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { · 20 unmodified lines
56 style={{ minHeight: SOURCE_LINE_HEIGHT }}57 style={{ minHeight: SOURCE_LINE_HEIGHT }}
57 >58 >
58 <NativeText59 <NativeText
59 className="select-none pr-3 text-right text-[11px] leading-[24px] text-foreground-tertiary"60 className="select-none pr-3 text-right text-foreground-tertiary"
60 style={{61 style={{
61 width: SOURCE_LINE_NUMBER_WIDTH,62 width: SOURCE_LINE_NUMBER_WIDTH,
62 fontFamily: REVIEW_MONO_FONT_FAMILY,63 fontFamily: REVIEW_MONO_FONT_FAMILY,
64 fontSize: MOBILE_CODE_SURFACE.lineNumberFontSize,
65 lineHeight: MOBILE_CODE_SURFACE.rowHeight,
63 }}66 }}
64 >67 >
65 {props.index + 1}68 {props.index + 1}
@@ -67,8 +70,13 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { · 1 unmodified line
67 <NativeText70 <NativeText
68 selectable71 selectable
69 numberOfLines={1}72 numberOfLines={1}
70 className="text-[13px] font-medium leading-[24px] text-foreground"73 className="font-normal text-foreground"
71 style={{ fontFamily: REVIEW_MONO_FONT_FAMILY, minWidth: 320 }}74 style={{
75 fontFamily: REVIEW_MONO_FONT_FAMILY,
76 fontSize: MOBILE_CODE_SURFACE.fontSize,
77 lineHeight: MOBILE_CODE_SURFACE.rowHeight,
78 minWidth: 320,
79 }}
72 >80 >
73 {props.tokens && props.tokens.length > 081 {props.tokens && props.tokens.length > 0
74 ? (() => {82 ? (() => {
@@ -80,7 +88,7 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { · 5 unmodified lines
80 const fontWeight =88 const fontWeight =
81 token.fontStyle !== null && (token.fontStyle & 2) === 289 token.fontStyle !== null && (token.fontStyle & 2) === 2
82 ? ("700" as const)90 ? ("700" as const)
83 : ("500" as const);91 : ("400" as const);
84 const fontStyle =92 const fontStyle =
85 token.fontStyle !== null && (token.fontStyle & 1) === 193 token.fontStyle !== null && (token.fontStyle & 1) === 1
86 ? ("italic" as const)94 ? ("italic" as const)
@@ -142,9 +150,7 @@ function SourceHighlightStatusView(props: { readonly status: SourceHighlightStat · 55 unmodified lines
142 if (props.status === "error") {150 if (props.status === "error") {
143 return (151 return (
144 <View className="border-b border-border bg-card px-4 py-2">152 <View className="border-b border-border bg-card px-4 py-2">
145 <Text className="text-[11px] font-t3-medium uppercase text-foreground-muted">153 <Text className="text-2xs font-t3-medium uppercase text-foreground-muted">Plain text</Text>
146 Plain text
147 </Text>
148 </View>154 </View>
149 );155 );
150 }156 }
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+8−7
@@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState"; · 23 unmodified lines
24import { LoadingScreen } from "../../components/LoadingScreen";24import { LoadingScreen } from "../../components/LoadingScreen";
25import { cn } from "../../lib/cn";25import { cn } from "../../lib/cn";
26import { buildThreadFilesNavigation } from "../../lib/routes";26import { buildThreadFilesNavigation } from "../../lib/routes";
27import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
27import { useThemeColor } from "../../lib/useThemeColor";28import { useThemeColor } from "../../lib/useThemeColor";
28import { useThreadSelection } from "../../state/use-thread-selection";29import { useThreadSelection } from "../../state/use-thread-selection";
29import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";30import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
@@ -100,7 +101,7 @@ function ModeButton(props: { · 70 unmodified lines
100 <SymbolView name={props.icon} size={13} tintColor={iconColor} type="monochrome" />101 <SymbolView name={props.icon} size={13} tintColor={iconColor} type="monochrome" />
101 <Text102 <Text
102 className={cn(103 className={cn(
103 "text-[12px] font-t3-bold",104 "text-xs font-t3-bold",
104 props.active ? "text-primary-foreground" : "text-foreground-muted",105 props.active ? "text-primary-foreground" : "text-foreground-muted",
105 )}106 )}
106 >107 >
@@ -187,7 +188,7 @@ function FileBreadcrumbs(props: { readonly projectName: string; readonly relativ · 80 unmodified lines
187 ) : null}188 ) : null}
188 <Text189 <Text
189 className={cn(190 className={cn(
190 "max-w-[180px] px-1 text-[12px]",191 "max-w-[180px] px-1 text-xs",
191 crumb.kind === "file"192 crumb.kind === "file"
192 ? "font-t3-bold text-foreground"193 ? "font-t3-bold text-foreground"
193 : "font-t3-medium text-foreground-muted",194 : "font-t3-medium text-foreground-muted",
@@ -308,7 +309,7 @@ function FileContent(props: { · 114 unmodified lines
308 return (309 return (
309 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">310 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">
310 <ActivityIndicator />311 <ActivityIndicator />
311 <Text className="text-center text-[13px] text-foreground-muted">Loading file...</Text>312 <Text className="text-center text-sm text-foreground-muted">Loading file...</Text>
312 </View>313 </View>
313 );314 );
314 }315 }
@@ -317,10 +318,10 @@ function FileContent(props: { · 2 unmodified lines
317 <View className="flex-1 bg-card">318 <View className="flex-1 bg-card">
318 {props.truncated ? (319 {props.truncated ? (
319 <View className="border-b border-amber-200 bg-amber-50 px-4 py-2 dark:border-amber-900/60 dark:bg-amber-950/40">320 <View className="border-b border-amber-200 bg-amber-50 px-4 py-2 dark:border-amber-900/60 dark:bg-amber-950/40">
320 <Text className="text-[11px] font-t3-bold uppercase text-amber-700 dark:text-amber-300">321 <Text className="text-2xs font-t3-bold uppercase text-amber-700 dark:text-amber-300">
321 Partial file322 Partial file
322 </Text>323 </Text>
323 <Text className="text-[12px] leading-[17px] text-amber-800 dark:text-amber-200">324 <Text className="text-xs leading-[17px] text-amber-800 dark:text-amber-200">
324 Preview limited to the first 1 MB of a truncated file.325 Preview limited to the first 1 MB of a truncated file.
325 </Text>326 </Text>
326 </View>327 </View>
@@ -389,7 +390,7 @@ function FilesHeaderTitle(props: { readonly projectName: string }) { · 62 unmodified lines
389 style={{390 style={{
390 color: foregroundColor,391 color: foregroundColor,
391 fontFamily: "DMSans_700Bold",392 fontFamily: "DMSans_700Bold",
392 fontSize: 18,393 fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
393 fontWeight: "900",394 fontWeight: "900",
394 letterSpacing: -0.4,395 letterSpacing: -0.4,
395 }}396 }}
@@ -401,7 +402,7 @@ function FilesHeaderTitle(props: { readonly projectName: string }) { · 5 unmodified lines
401 style={{402 style={{
402 color: secondaryForegroundColor,403 color: secondaryForegroundColor,
403 fontFamily: "DMSans_500Medium",404 fontFamily: "DMSans_500Medium",
404 fontSize: 12,405 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
405 fontWeight: "500",406 fontWeight: "500",
406 letterSpacing: 0.2,407 letterSpacing: 0.2,
407 }}408 }}
apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx+2−2
@@ -81,7 +81,7 @@ function CachedWorkspaceFileImagePreview(props: { · 80 unmodified lines
81 return (81 return (
82 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">82 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">
83 <ActivityIndicator />83 <ActivityIndicator />
84 <Text className="text-center text-[13px] text-foreground-muted">Loading image...</Text>84 <Text className="text-center text-sm text-foreground-muted">Loading image...</Text>
85 </View>85 </View>
86 );86 );
87 }87 }
@@ -102,7 +102,7 @@ export function WorkspaceFileImagePreview(props: { · 14 unmodified lines
102 return (102 return (
103 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">103 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">
104 <ActivityIndicator />104 <ActivityIndicator />
105 <Text className="text-center text-[13px] text-foreground-muted">105 <Text className="text-center text-sm text-foreground-muted">
106 Preparing image preview...106 Preparing image preview...
107 </Text>107 </Text>
108 </View>108 </View>
apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx+3−5
@@ -13,7 +13,7 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) · 12 unmodified lines
13 return (13 return (
14 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">14 <View className="flex-1 items-center justify-center gap-3 bg-card px-6">
15 <ActivityIndicator />15 <ActivityIndicator />
16 <Text className="text-center text-[13px] text-foreground-muted">Preparing preview...</Text>16 <Text className="text-center text-sm text-foreground-muted">Preparing preview...</Text>
17 </View>17 </View>
18 );18 );
19 }19 }
@@ -23,10 +23,8 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) · 3 unmodified lines
23 {loadProgress > 0 && loadProgress < 1 ? <LoadingStrip progress={loadProgress} /> : null}23 {loadProgress > 0 && loadProgress < 1 ? <LoadingStrip progress={loadProgress} /> : null}
24 {loadError ? (24 {loadError ? (
25 <View className="border-b border-border bg-card px-4 py-2">25 <View className="border-b border-border bg-card px-4 py-2">
26 <Text className="text-[12px] font-t3-bold text-foreground">Preview failed</Text>26 <Text className="text-xs font-t3-bold text-foreground">Preview failed</Text>
27 <Text className="mt-0.5 text-[12px] leading-[17px] text-foreground-muted">27 <Text className="mt-0.5 text-xs leading-[17px] text-foreground-muted">{loadError}</Text>
28 {loadError}
29 </Text>
30 </View>28 </View>
31 ) : null}29 ) : null}
32 <WebView30 <WebView
apps/mobile/src/features/files/nativeSourceFileAdapter.test.ts+20−0
@@ -3,10 +3,30 @@ import { describe, expect, it } from "vite-plus/test"; · 2 unmodified lines
3import {3import {
4 buildNativeSourceRows,4 buildNativeSourceRows,
5 buildNativeSourceTokens,5 buildNativeSourceTokens,
6 NATIVE_SOURCE_ROW_HEIGHT,
7 NATIVE_SOURCE_STYLE,
6 nativeSourceRowId,8 nativeSourceRowId,
7} from "./nativeSourceFileAdapter";9} from "./nativeSourceFileAdapter";
10import {
11 NATIVE_REVIEW_DIFF_ROW_HEIGHT,
12 NATIVE_REVIEW_DIFF_STYLE,
13} from "../review/nativeReviewDiffAdapter";
8 14
9describe("nativeSourceFileAdapter", () => {15describe("nativeSourceFileAdapter", () => {
16 it("uses the same compact code typography as the diff viewer", () => {
17 expect(NATIVE_SOURCE_ROW_HEIGHT).toBe(NATIVE_REVIEW_DIFF_ROW_HEIGHT);
18 expect(NATIVE_SOURCE_STYLE).toMatchObject({
19 rowHeight: NATIVE_REVIEW_DIFF_STYLE.rowHeight,
20 gutterWidth: NATIVE_REVIEW_DIFF_STYLE.gutterWidth,
21 codePadding: NATIVE_REVIEW_DIFF_STYLE.codePadding,
22 textVerticalInset: NATIVE_REVIEW_DIFF_STYLE.textVerticalInset,
23 codeFontSize: NATIVE_REVIEW_DIFF_STYLE.codeFontSize,
24 codeFontWeight: NATIVE_REVIEW_DIFF_STYLE.codeFontWeight,
25 lineNumberFontSize: NATIVE_REVIEW_DIFF_STYLE.lineNumberFontSize,
26 lineNumberFontWeight: NATIVE_REVIEW_DIFF_STYLE.lineNumberFontWeight,
27 });
28 });
29
10 it("maps plain source lines onto context rows with stable line numbers", () => {30 it("maps plain source lines onto context rows with stable line numbers", () => {
11 expect(buildNativeSourceRows(["const value = 1;", "\treturn value;"])).toEqual([31 expect(buildNativeSourceRows(["const value = 1;", "\treturn value;"])).toEqual([
12 {32 {
apps/mobile/src/features/files/nativeSourceFileAdapter.ts+10−8
@@ -3,22 +3,24 @@ import type { · 2 unmodified lines
3 NativeReviewDiffStyle,3 NativeReviewDiffStyle,
4 NativeReviewDiffToken,4 NativeReviewDiffToken,
5} from "../diffs/nativeReviewDiffSurface";5} from "../diffs/nativeReviewDiffSurface";
6import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "../../lib/typography";
6import type { SourceHighlightTokens } from "./sourceHighlightingState";7import type { SourceHighlightTokens } from "./sourceHighlightingState";
7 8
8export const NATIVE_SOURCE_ROW_HEIGHT = 24;9export const NATIVE_SOURCE_ROW_HEIGHT = MOBILE_CODE_SURFACE.rowHeight;
9export const NATIVE_SOURCE_CONTENT_WIDTH = 32_000;10export const NATIVE_SOURCE_CONTENT_WIDTH = 32_000;
10 11
11export const NATIVE_SOURCE_STYLE: NativeReviewDiffStyle = {12export const NATIVE_SOURCE_STYLE: NativeReviewDiffStyle = {
12 rowHeight: NATIVE_SOURCE_ROW_HEIGHT,13 rowHeight: NATIVE_SOURCE_ROW_HEIGHT,
13 contentWidth: NATIVE_SOURCE_CONTENT_WIDTH,14 contentWidth: NATIVE_SOURCE_CONTENT_WIDTH,
14 changeBarWidth: 0,15 changeBarWidth: 0,
15 gutterWidth: 58,16 gutterWidth: MOBILE_CODE_SURFACE.gutterWidth,
16 codePadding: 8,17 codePadding: MOBILE_CODE_SURFACE.codePadding,
17 codeFontSize: 13,18 textVerticalInset: MOBILE_CODE_SURFACE.textVerticalInset,
18 codeFontWeight: "medium",19 codeFontSize: MOBILE_CODE_SURFACE.fontSize,
19 lineNumberFontSize: 11,20 codeFontWeight: "regular",
20 lineNumberFontWeight: "medium",21 lineNumberFontSize: MOBILE_CODE_SURFACE.lineNumberFontSize,
21 emptyStateFontSize: 12,22 lineNumberFontWeight: "regular",
23 emptyStateFontSize: MOBILE_TYPOGRAPHY.label.fontSize,
22 emptyStateFontWeight: "medium",24 emptyStateFontWeight: "medium",
23};25};
24 26
apps/mobile/src/features/home/HomeHeader.tsx+3−2
@@ -12,6 +12,7 @@ import { Stack } from "expo-router"; · 11 unmodified lines
12import { Text as RNText, View } from "react-native";12import { Text as RNText, View } from "react-native";
13 13
14import { useThemeColor } from "../../lib/useThemeColor";14import { useThemeColor } from "../../lib/useThemeColor";
15import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
15import type { HomeProjectSortOrder } from "./homeThreadList";16import type { HomeProjectSortOrder } from "./homeThreadList";
16 17
17export interface HomeHeaderEnvironment {18export interface HomeHeaderEnvironment {
@@ -118,7 +119,7 @@ export function HomeHeader(props: { · 100 unmodified lines
118 <RNText119 <RNText
119 style={{120 style={{
120 fontFamily: "DMSans_700Bold",121 fontFamily: "DMSans_700Bold",
121 fontSize: 17,122 fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
122 color: iconColor,123 color: iconColor,
123 letterSpacing: -0.4,124 letterSpacing: -0.4,
124 }}125 }}
@@ -136,7 +137,7 @@ export function HomeHeader(props: { · 11 unmodified lines
136 <RNText137 <RNText
137 style={{138 style={{
138 fontFamily: "DMSans_700Bold",139 fontFamily: "DMSans_700Bold",
139 fontSize: 10,140 fontSize: MOBILE_TYPOGRAPHY.micro.fontSize,
140 color: mutedColor,141 color: mutedColor,
141 letterSpacing: 1.1,142 letterSpacing: 1.1,
142 textTransform: "uppercase",143 textTransform: "uppercase",
apps/mobile/src/features/home/HomeScreen.tsx+7−7
@@ -186,7 +186,7 @@ function ProjectGroupLabel(props: { · 185 unmodified lines
186 workspaceRoot={props.project.workspaceRoot}186 workspaceRoot={props.project.workspaceRoot}
187 />187 />
188 <Text188 <Text
189 className="flex-1 text-[12px] font-t3-medium uppercase text-foreground-muted"189 className="flex-1 text-xs font-t3-medium uppercase text-foreground-muted"
190 style={{ letterSpacing: 0.5 }}190 style={{ letterSpacing: 0.5 }}
191 numberOfLines={1}191 numberOfLines={1}
192 >192 >
@@ -196,7 +196,7 @@ function ProjectGroupLabel(props: { · 3 unmodified lines
196 {hiddenCount > 0 ? (196 {hiddenCount > 0 ? (
197 <Pressable onPress={props.onToggleExpand} hitSlop={8}>197 <Pressable onPress={props.onToggleExpand} hitSlop={8}>
198 <Text198 <Text
199 className="text-[12px] font-t3-medium text-foreground-muted"199 className="text-xs font-t3-medium text-foreground-muted"
200 style={{ letterSpacing: 0.4 }}200 style={{ letterSpacing: 0.4 }}
201 >201 >
202 {props.isExpanded ? "Show less" : `${hiddenCount} more`}202 {props.isExpanded ? "Show less" : `${hiddenCount} more`}
@@ -335,7 +335,7 @@ function ThreadRow(props: { · 132 unmodified lines
335 <View style={{ flex: 1, gap: 3 }}>335 <View style={{ flex: 1, gap: 3 }}>
336 <View className="flex-row items-center justify-between gap-2">336 <View className="flex-row items-center justify-between gap-2">
337 <Text337 <Text
338 className="flex-1 text-[15px] font-t3-bold leading-[20px] text-foreground"338 className="flex-1 text-base font-t3-bold leading-[20px] text-foreground"
339 numberOfLines={1}339 numberOfLines={1}
340 >340 >
341 {props.thread.title}341 {props.thread.title}
@@ -345,12 +345,12 @@ function ThreadRow(props: { · 3 unmodified lines
345 className={tone.pillClassName}345 className={tone.pillClassName}
346 style={{ borderRadius: 99, paddingHorizontal: 6, paddingVertical: 2 }}346 style={{ borderRadius: 99, paddingHorizontal: 6, paddingVertical: 2 }}
347 >347 >
348 <Text className={`text-[10px] font-t3-bold ${tone.textClassName}`}>348 <Text className={`text-3xs font-t3-bold ${tone.textClassName}`}>
349 {tone.label}349 {tone.label}
350 </Text>350 </Text>
351 </View>351 </View>
352 <Text352 <Text
353 className="text-[12px] text-foreground-tertiary"353 className="text-xs text-foreground-tertiary"
354 style={{ fontVariant: ["tabular-nums"] }}354 style={{ fontVariant: ["tabular-nums"] }}
355 >355 >
356 {timestamp}356 {timestamp}
@@ -367,7 +367,7 @@ function ThreadRow(props: { · 10 unmodified lines
367 type="monochrome"367 type="monochrome"
368 />368 />
369 <Text369 <Text
370 className="text-[11px] text-foreground-tertiary"370 className="text-2xs text-foreground-tertiary"
371 numberOfLines={1}371 numberOfLines={1}
372 style={{ fontFamily: "monospace" }}372 style={{ fontFamily: "monospace" }}
373 >373 >
@@ -429,7 +429,7 @@ function StaleCatalogStatusPill(props: { · 55 unmodified lines
429 weight="semibold"429 weight="semibold"
430 />430 />
431 )}431 )}
432 <Text className="max-w-[260px] text-[13px] font-t3-bold text-foreground" numberOfLines={1}>432 <Text className="max-w-[260px] text-sm font-t3-bold text-foreground" numberOfLines={1}>
433 {label}433 {label}
434 </Text>434 </Text>
435 </Pressable>435 </Pressable>
apps/mobile/src/features/home/thread-swipe-actions.tsx+1−1
@@ -167,7 +167,7 @@ function SwipeActionButton(props: { · 166 unmodified lines
167 </Animated.View>167 </Animated.View>
168 </View>168 </View>
169 <Animated.View style={[{ paddingTop: 2 }, labelStyle]}>169 <Animated.View style={[{ paddingTop: 2 }, labelStyle]}>
170 <Text className="text-[10px] font-t3-medium text-foreground-muted">{props.label}</Text>170 <Text className="text-3xs font-t3-medium text-foreground-muted">{props.label}</Text>
171 </Animated.View>171 </Animated.View>
172 </Pressable>172 </Pressable>
173 </Animated.View>173 </Animated.View>
apps/mobile/src/features/projects/AddProjectScreen.tsx+11−11
@@ -100,7 +100,7 @@ function sourceFromParam(value: string | string[] | undefined): AddProjectRemote · 99 unmodified lines
100function SectionTitle(props: { readonly children: string }) {100function SectionTitle(props: { readonly children: string }) {
101 return (101 return (
102 <Text102 <Text
103 className="px-1 text-[11px] font-t3-bold uppercase text-foreground-muted"103 className="px-1 text-2xs font-t3-bold uppercase text-foreground-muted"
104 style={{ letterSpacing: 0.7 }}104 style={{ letterSpacing: 0.7 }}
105 >105 >
106 {props.children}106 {props.children}
@@ -168,9 +168,9 @@ function ListRow(props: { · 61 unmodified lines
168 {props.icon}168 {props.icon}
169 </View>169 </View>
170 <View className="flex-1 gap-0.5">170 <View className="flex-1 gap-0.5">
171 <Text className="text-[16px] leading-[21px] font-t3-bold">{props.title}</Text>171 <Text className="text-base leading-[21px] font-t3-bold">{props.title}</Text>
172 {props.subtitle ? (172 {props.subtitle ? (
173 <Text className="text-[13px] leading-[17px] text-foreground-muted" numberOfLines={2}>173 <Text className="text-sm leading-[17px] text-foreground-muted" numberOfLines={2}>
174 {props.subtitle}174 {props.subtitle}
175 </Text>175 </Text>
176 ) : null}176 ) : null}
@@ -202,7 +202,7 @@ function PrimaryActionButton(props: { · 25 unmodified lines
202 {props.loading ? (202 {props.loading ? (
203 <ActivityIndicator color={String(primaryForeground)} />203 <ActivityIndicator color={String(primaryForeground)} />
204 ) : (204 ) : (
205 <Text className="text-[14px] font-t3-bold text-primary-foreground">{props.label}</Text>205 <Text className="text-base font-t3-bold text-primary-foreground">{props.label}</Text>
206 )}206 )}
207 </Pressable>207 </Pressable>
208 );208 );
@@ -215,7 +215,7 @@ function ProjectPathInput(props: { · 6 unmodified lines
215}) {215}) {
216 return (216 return (
217 <TextInput217 <TextInput
218 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-[15px] leading-[20px]"218 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-[20px]"
219 value={props.value}219 value={props.value}
220 onChangeText={props.onChangeText}220 onChangeText={props.onChangeText}
221 autoCapitalize="none"221 autoCapitalize="none"
@@ -273,15 +273,15 @@ function EmptyEnvironmentState() { · 51 unmodified lines
273 273
274 return (274 return (
275 <View className="items-center gap-3 rounded-2xl bg-card px-5 py-8">275 <View className="items-center gap-3 rounded-2xl bg-card px-5 py-8">
276 <Text className="text-center text-[17px] font-t3-bold">No environments connected</Text>276 <Text className="text-center text-lg font-t3-bold">No environments connected</Text>
277 <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">277 <Text className="text-center text-sm leading-[20px] text-foreground-muted">
278 Add an environment before adding a project.278 Add an environment before adding a project.
279 </Text>279 </Text>
280 <Pressable280 <Pressable
281 onPress={() => router.replace("/connections/new")}281 onPress={() => router.replace("/connections/new")}
282 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"282 className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70"
283 >283 >
284 <Text className="text-[13px] font-t3-bold text-primary-foreground">Add environment</Text>284 <Text className="text-sm font-t3-bold text-primary-foreground">Add environment</Text>
285 </Pressable>285 </Pressable>
286 </View>286 </View>
287 );287 );
@@ -565,7 +565,7 @@ export function AddProjectRepositoryScreen() { · 277 unmodified lines
565 <AddProjectShell>565 <AddProjectShell>
566 {error ? <ErrorBanner message={error} /> : null}566 {error ? <ErrorBanner message={error} /> : null}
567 <TextInput567 <TextInput
568 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-[15px] leading-[20px]"568 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-[20px]"
569 value={repositoryInput}569 value={repositoryInput}
570 onChangeText={setRepositoryInput}570 onChangeText={setRepositoryInput}
571 autoCapitalize="none"571 autoCapitalize="none"
@@ -806,8 +806,8 @@ export function AddProjectDestinationScreen() { · 234 unmodified lines
806 {error ? <ErrorBanner message={error} /> : null}806 {error ? <ErrorBanner message={error} /> : null}
807 {repositoryTitle ? (807 {repositoryTitle ? (
808 <View className="rounded-[24px] bg-card px-4 py-3">808 <View className="rounded-[24px] bg-card px-4 py-3">
809 <Text className="text-[14px] font-t3-bold">{repositoryTitle}</Text>809 <Text className="text-base font-t3-bold">{repositoryTitle}</Text>
810 <Text className="mt-0.5 text-[12px] text-foreground-muted" numberOfLines={2}>810 <Text className="mt-0.5 text-xs text-foreground-muted" numberOfLines={2}>
811 {remoteUrl}811 {remoteUrl}
812 </Text>812 </Text>
813 </View>813 </View>
apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx+8−8
@@ -159,26 +159,26 @@ export function ReviewCommentComposerSheet() { · 158 unmodified lines
159 <SymbolView name="xmark" size={18} tintColor={iconTint} type="monochrome" />159 <SymbolView name="xmark" size={18} tintColor={iconTint} type="monochrome" />
160 </Pressable>160 </Pressable>
161 161
162 <Text className="text-[18px] font-t3-bold text-foreground">Add Comment</Text>162 <Text className="text-lg font-t3-bold text-foreground">Add Comment</Text>
163 163
164 <View className="h-12 w-12" />164 <View className="h-12 w-12" />
165 </View>165 </View>
166 166
167 {!target ? (167 {!target ? (
168 <View className="rounded-[22px] border border-border bg-card px-4 py-5">168 <View className="rounded-[22px] border border-border bg-card px-4 py-5">
169 <Text className="text-[15px] font-t3-bold text-foreground">No selection</Text>169 <Text className="text-base font-t3-bold text-foreground">No selection</Text>
170 <Text className="mt-1 text-[13px] leading-[19px] text-foreground-muted">170 <Text className="mt-1 text-sm leading-[19px] text-foreground-muted">
171 Select a diff line or range first.171 Select a diff line or range first.
172 </Text>172 </Text>
173 </View>173 </View>
174 ) : (174 ) : (
175 <View className="min-h-0 flex-1 gap-4">175 <View className="min-h-0 flex-1 gap-4">
176 <View className="gap-1 px-1">176 <View className="gap-1 px-1">
177 <Text className="text-[11px] font-t3-bold uppercase text-foreground-muted">177 <Text className="text-2xs font-t3-bold uppercase text-foreground-muted">
178 {selectionLabel}178 {selectionLabel}
179 </Text>179 </Text>
180 <Text180 <Text
181 className="font-mono text-[12px] leading-[17px] text-foreground-muted"181 className="font-mono text-xs leading-[17px] text-foreground-muted"
182 ellipsizeMode="middle"182 ellipsizeMode="middle"
183 numberOfLines={2}183 numberOfLines={2}
184 >184 >
@@ -215,7 +215,7 @@ export function ReviewCommentComposerSheet() { · 30 unmodified lines
215 >215 >
216 <ReviewChangeBar change={line.change} />216 <ReviewChangeBar change={line.change} />
217 <Text217 <Text
218 className="w-9 py-1 pr-1 text-right text-[11px] font-t3-medium text-foreground-muted"218 className="w-9 py-1 pr-1 text-right text-2xs font-t3-medium text-foreground-muted"
219 style={{ fontFamily: REVIEW_MONO_FONT_FAMILY }}219 style={{ fontFamily: REVIEW_MONO_FONT_FAMILY }}
220 >220 >
221 {lineNumber ?? ""}221 {lineNumber ?? ""}
@@ -236,7 +236,7 @@ export function ReviewCommentComposerSheet() { · 14 unmodified lines
236 </View>236 </View>
237 237
238 <View className="min-h-0 flex-1 gap-2">238 <View className="min-h-0 flex-1 gap-2">
239 <Text className="text-[13px] font-t3-bold text-foreground">Comment</Text>239 <Text className="text-sm font-t3-bold text-foreground">Comment</Text>
240 <View className="min-h-[132px] flex-1 overflow-hidden rounded-[20px] border border-border bg-card">240 <View className="min-h-[132px] flex-1 overflow-hidden rounded-[20px] border border-border bg-card">
241 <View className="min-h-0 flex-1 px-4 pt-3.5">241 <View className="min-h-0 flex-1 px-4 pt-3.5">
242 <TextInputWrapper onPaste={handleNativePaste} style={{ flex: 1, minHeight: 0 }}>242 <TextInputWrapper onPaste={handleNativePaste} style={{ flex: 1, minHeight: 0 }}>
@@ -248,7 +248,7 @@ export function ReviewCommentComposerSheet() { · 5 unmodified lines
248 textAlignVertical="top"248 textAlignVertical="top"
249 value={commentText}249 value={commentText}
250 onChangeText={setCommentText}250 onChangeText={setCommentText}
251 className="h-full flex-1 border-0 bg-transparent px-0 py-0 font-sans text-[15px]"251 className="h-full flex-1 border-0 bg-transparent px-0 py-0 font-sans text-base"
252 style={{ flex: 1, minHeight: 0 }}252 style={{ flex: 1, minHeight: 0 }}
253 />253 />
254 </TextInputWrapper>254 </TextInputWrapper>
apps/mobile/src/features/review/ReviewSheet.tsx+19−18
@@ -20,6 +20,7 @@ import { environmentCatalog } from "../../connection/catalog"; · 19 unmodified lines
20import { useEnvironmentPresentation } from "../../state/presentation";20import { useEnvironmentPresentation } from "../../state/presentation";
21import { useAtomCommand } from "../../state/use-atom-command";21import { useAtomCommand } from "../../state/use-atom-command";
22import { useThemeColor } from "../../lib/useThemeColor";22import { useThemeColor } from "../../lib/useThemeColor";
23import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
23import { useThreadDraftForThread } from "../../state/use-thread-composer-state";24import { useThreadDraftForThread } from "../../state/use-thread-composer-state";
24import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";25import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";
25import { useReviewCacheForThread } from "./reviewState";26import { useReviewCacheForThread } from "./reviewState";
@@ -41,10 +42,10 @@ const REVIEW_HEADER_SPACING = 0; · 15 unmodified lines
41const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {42const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {
42 return (43 return (
43 <View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40">44 <View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40">
44 <Text className="text-[12px] font-t3-bold uppercase text-amber-700 dark:text-amber-300">45 <Text className="text-xs font-t3-bold uppercase text-amber-700 dark:text-amber-300">
45 Partial diff46 Partial diff
46 </Text>47 </Text>
47 <Text className="text-[12px] leading-[18px] text-amber-800 dark:text-amber-200">48 <Text className="text-xs leading-[18px] text-amber-800 dark:text-amber-200">
48 {props.notice}49 {props.notice}
49 </Text>50 </Text>
50 </View>51 </View>
@@ -69,7 +70,7 @@ function ReviewSelectionActionBar(props: { · 18 unmodified lines
69 tintColor="#ffffff"70 tintColor="#ffffff"
70 type="monochrome"71 type="monochrome"
71 />72 />
72 <Text className="text-[15px] font-t3-bold text-white">{props.title}</Text>73 <Text className="text-base font-t3-bold text-white">{props.title}</Text>
73 </>74 </>
74 );75 );
75 76
@@ -218,8 +219,8 @@ export function ReviewSheet() { · 142 unmodified lines
218 if (error) {219 if (error) {
219 children.push(220 children.push(
220 <View key="review-error" className="border-b border-border bg-card px-4 py-3">221 <View key="review-error" className="border-b border-border bg-card px-4 py-3">
221 <Text className="text-[13px] font-t3-bold text-foreground">Review unavailable</Text>222 <Text className="text-sm font-t3-bold text-foreground">Review unavailable</Text>
222 <Text className="text-[12px] leading-[18px] text-foreground-muted">{error}</Text>223 <Text className="text-xs leading-[18px] text-foreground-muted">{error}</Text>
223 </View>,224 </View>,
224 );225 );
225 }226 }
@@ -251,7 +252,7 @@ export function ReviewSheet() { · 25 unmodified lines
251 numberOfLines={1}252 numberOfLines={1}
252 style={{253 style={{
253 fontFamily: "DMSans_700Bold",254 fontFamily: "DMSans_700Bold",
254 fontSize: 18,255 fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
255 fontWeight: "900",256 fontWeight: "900",
256 color: headerForeground,257 color: headerForeground,
257 letterSpacing: -0.4,258 letterSpacing: -0.4,
@@ -273,7 +274,7 @@ export function ReviewSheet() { · 15 unmodified lines
273 <NativeText274 <NativeText
274 style={{275 style={{
275 fontFamily: "DMSans_700Bold",276 fontFamily: "DMSans_700Bold",
276 fontSize: 12,277 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
277 fontWeight: "700",278 fontWeight: "700",
278 color: "#16a34a",279 color: "#16a34a",
279 }}280 }}
@@ -283,7 +284,7 @@ export function ReviewSheet() { · 3 unmodified lines
283 <NativeText284 <NativeText
284 style={{285 style={{
285 fontFamily: "DMSans_700Bold",286 fontFamily: "DMSans_700Bold",
286 fontSize: 12,287 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
287 fontWeight: "700",288 fontWeight: "700",
288 color: "#e11d48",289 color: "#e11d48",
289 }}290 }}
@@ -294,7 +295,7 @@ export function ReviewSheet() { · 4 unmodified lines
294 <NativeText295 <NativeText
295 style={{296 style={{
296 fontFamily: "DMSans_700Bold",297 fontFamily: "DMSans_700Bold",
297 fontSize: 12,298 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
298 fontWeight: "700",299 fontWeight: "700",
299 color: "#b45309",300 color: "#b45309",
300 }}301 }}
@@ -309,7 +310,7 @@ export function ReviewSheet() { · 8 unmodified lines
309 numberOfLines={1}310 numberOfLines={1}
310 style={{311 style={{
311 fontFamily: "DMSans_700Bold",312 fontFamily: "DMSans_700Bold",
312 fontSize: 12,313 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
313 fontWeight: "700",314 fontWeight: "700",
314 color: headerMuted,315 color: headerMuted,
315 }}316 }}
@@ -320,7 +321,7 @@ export function ReviewSheet() { · 4 unmodified lines
320 <NativeText321 <NativeText
321 style={{322 style={{
322 fontFamily: "DMSans_700Bold",323 fontFamily: "DMSans_700Bold",
323 fontSize: 12,324 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
324 fontWeight: "700",325 fontWeight: "700",
325 color: "#b45309",326 color: "#b45309",
326 }}327 }}
@@ -430,30 +431,30 @@ export function ReviewSheet() { · 103 unmodified lines
430 {listHeader}431 {listHeader}
431 {!selectedSection ? (432 {!selectedSection ? (
432 <View className="border-b border-border bg-card px-4 py-5">433 <View className="border-b border-border bg-card px-4 py-5">
433 <Text className="text-[14px] font-t3-bold text-foreground">No review diffs</Text>434 <Text className="text-sm font-t3-bold text-foreground">No review diffs</Text>
434 <Text className="text-[12px] leading-[18px] text-foreground-muted">435 <Text className="text-xs leading-[18px] text-foreground-muted">
435 This thread has no ready turn diffs and the worktree diff is empty.436 This thread has no ready turn diffs and the worktree diff is empty.
436 </Text>437 </Text>
437 </View>438 </View>
438 ) : selectedSection.isLoading && selectedSection.diff === null ? (439 ) : selectedSection.isLoading && selectedSection.diff === null ? (
439 <View className="items-center gap-3 border-b border-border bg-card px-4 py-6">440 <View className="items-center gap-3 border-b border-border bg-card px-4 py-6">
440 <ActivityIndicator size="small" />441 <ActivityIndicator size="small" />
441 <Text className="text-[12px] text-foreground-muted">Loading diff…</Text>442 <Text className="text-xs text-foreground-muted">Loading diff…</Text>
442 </View>443 </View>
443 ) : parsedDiff.kind === "empty" ? (444 ) : parsedDiff.kind === "empty" ? (
444 <View className="border-b border-border bg-card px-4 py-5">445 <View className="border-b border-border bg-card px-4 py-5">
445 <Text className="text-[14px] font-t3-bold text-foreground">No changes</Text>446 <Text className="text-sm font-t3-bold text-foreground">No changes</Text>
446 <Text className="text-[12px] leading-[18px] text-foreground-muted">447 <Text className="text-xs leading-[18px] text-foreground-muted">
447 {selectedSection.subtitle ?? "This diff is empty."}448 {selectedSection.subtitle ?? "This diff is empty."}
448 </Text>449 </Text>
449 </View>450 </View>
450 ) : parsedDiff.kind === "raw" ? (451 ) : parsedDiff.kind === "raw" ? (
451 <View className="gap-3 border-b border-border bg-card px-4 py-4">452 <View className="gap-3 border-b border-border bg-card px-4 py-4">
452 <Text className="text-[12px] leading-[18px] text-foreground-muted">453 <Text className="text-xs leading-[18px] text-foreground-muted">
453 {parsedDiff.reason}454 {parsedDiff.reason}
454 </Text>455 </Text>
455 <ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}>456 <ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}>
456 <Text selectable className="font-mono text-[12px] leading-[19px] text-foreground">457 <Text selectable className="font-mono text-xs leading-[19px] text-foreground">
457 {parsedDiff.text}458 {parsedDiff.text}
458 </Text>459 </Text>
459 </ScrollView>460 </ScrollView>
apps/mobile/src/features/review/nativeReviewDiffAdapter.ts+7−6
@@ -5,6 +5,7 @@ import type { · 4 unmodified lines
5} from "../diffs/nativeReviewDiffTypes";5} from "../diffs/nativeReviewDiffTypes";
6import * as Arr from "effect/Array";6import * as Arr from "effect/Array";
7import { pipe } from "effect/Function";7import { pipe } from "effect/Function";
8import { MOBILE_CODE_SURFACE } from "../../lib/typography";
8import { getPierreTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme";9import { getPierreTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme";
9import { computeWordAltDiffRanges } from "./reviewWordDiffs";10import { computeWordAltDiffRanges } from "./reviewWordDiffs";
10import {11import {
@@ -18,16 +19,16 @@ import type { ReviewInlineComment } from "./reviewCommentSelection"; · 7 unmodified lines
18const NATIVE_REVIEW_MAX_WORD_DIFF_RANGE_COUNT = 4;19const NATIVE_REVIEW_MAX_WORD_DIFF_RANGE_COUNT = 4;
19const NATIVE_REVIEW_MAX_WORD_DIFF_COVERAGE = 0.45;20const NATIVE_REVIEW_MAX_WORD_DIFF_COVERAGE = 0.45;
20 21
21export const NATIVE_REVIEW_DIFF_ROW_HEIGHT = 20;22export const NATIVE_REVIEW_DIFF_ROW_HEIGHT = MOBILE_CODE_SURFACE.rowHeight;
22export const NATIVE_REVIEW_DIFF_CONTENT_WIDTH = 2_800;23export const NATIVE_REVIEW_DIFF_CONTENT_WIDTH = 2_800;
23 24
24export const NATIVE_REVIEW_DIFF_STYLE = {25export const NATIVE_REVIEW_DIFF_STYLE = {
25 rowHeight: NATIVE_REVIEW_DIFF_ROW_HEIGHT,26 rowHeight: NATIVE_REVIEW_DIFF_ROW_HEIGHT,
26 contentWidth: NATIVE_REVIEW_DIFF_CONTENT_WIDTH,27 contentWidth: NATIVE_REVIEW_DIFF_CONTENT_WIDTH,
27 changeBarWidth: 4,28 changeBarWidth: 4,
28 gutterWidth: 46,29 gutterWidth: MOBILE_CODE_SURFACE.gutterWidth,
29 codePadding: 7,30 codePadding: MOBILE_CODE_SURFACE.codePadding,
30 textVerticalInset: 2,31 textVerticalInset: MOBILE_CODE_SURFACE.textVerticalInset,
31 fileHeaderHeight: 56,32 fileHeaderHeight: 56,
32 fileHeaderHorizontalMargin: 8,33 fileHeaderHorizontalMargin: 8,
33 fileHeaderVerticalMargin: 6,34 fileHeaderVerticalMargin: 6,
@@ -36,9 +37,9 @@ export const NATIVE_REVIEW_DIFF_STYLE = { · 2 unmodified lines
36 fileHeaderPathRightPadding: 118,37 fileHeaderPathRightPadding: 118,
37 fileHeaderCountColumnWidth: 38,38 fileHeaderCountColumnWidth: 38,
38 fileHeaderCountGap: 5,39 fileHeaderCountGap: 5,
39 codeFontSize: 11,40 codeFontSize: MOBILE_CODE_SURFACE.fontSize,
40 codeFontWeight: "regular",41 codeFontWeight: "regular",
41 lineNumberFontSize: 10,42 lineNumberFontSize: MOBILE_CODE_SURFACE.lineNumberFontSize,
42 lineNumberFontWeight: "regular",43 lineNumberFontWeight: "regular",
43 hunkFontSize: 11,44 hunkFontSize: 11,
44 hunkFontWeight: "medium",45 hunkFontWeight: "medium",
apps/mobile/src/features/review/reviewDiffRendering.tsx+14−5
@@ -1,6 +1,7 @@
1import { Platform, Text as NativeText, View } from "react-native";1import { Platform, Text as NativeText, View } from "react-native";
2 2
3import { cn } from "../../lib/cn";3import { cn } from "../../lib/cn";
4import { MOBILE_CODE_SURFACE } from "../../lib/typography";
4 5
5import type { ReviewRenderableLineRow } from "./reviewModel";6import type { ReviewRenderableLineRow } from "./reviewModel";
6import type { ReviewHighlightedToken } from "./shikiReviewHighlighter";7import type { ReviewHighlightedToken } from "./shikiReviewHighlighter";
@@ -11,7 +12,7 @@ export const REVIEW_MONO_FONT_FAMILY = Platform.select({ · 4 unmodified lines
11 default: "monospace",12 default: "monospace",
12});13});
13 14
14export const REVIEW_DIFF_LINE_HEIGHT = 26;15export const REVIEW_DIFF_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight;
15const REVIEW_DELETE_STRIPE_COUNT = REVIEW_DIFF_LINE_HEIGHT / 2;16const REVIEW_DELETE_STRIPE_COUNT = REVIEW_DIFF_LINE_HEIGHT / 2;
16 17
17export function renderVisibleWhitespace(value: string): string {18export function renderVisibleWhitespace(value: string): string {
@@ -71,8 +72,12 @@ export function DiffTokenText(props: { · 53 unmodified lines
71 <NativeText72 <NativeText
72 numberOfLines={1}73 numberOfLines={1}
73 selectable74 selectable
74 className={cn("text-[13px] leading-[17px] font-medium text-foreground", props.className)}75 className={cn("font-normal text-foreground", props.className)}
75 style={{ fontFamily: REVIEW_MONO_FONT_FAMILY }}76 style={{
77 fontFamily: REVIEW_MONO_FONT_FAMILY,
78 fontSize: MOBILE_CODE_SURFACE.fontSize,
79 lineHeight: MOBILE_CODE_SURFACE.rowHeight,
80 }}
76 >81 >
77 {renderVisibleWhitespace(props.fallback || " ")}82 {renderVisibleWhitespace(props.fallback || " ")}
78 </NativeText>83 </NativeText>
@@ -83,8 +88,12 @@ export function DiffTokenText(props: { · 4 unmodified lines
83 <NativeText88 <NativeText
84 numberOfLines={1}89 numberOfLines={1}
85 selectable90 selectable
86 className={cn("text-[13px] leading-[17px] font-medium text-foreground", props.className)}91 className={cn("font-normal text-foreground", props.className)}
87 style={{ fontFamily: REVIEW_MONO_FONT_FAMILY }}92 style={{
93 fontFamily: REVIEW_MONO_FONT_FAMILY,
94 fontSize: MOBILE_CODE_SURFACE.fontSize,
95 lineHeight: MOBILE_CODE_SURFACE.rowHeight,
96 }}
88 >97 >
89 {(() => {98 {(() => {
90 let offset = 0;99 let offset = 0;
apps/mobile/src/features/terminal/NativeTerminalSurface.tsx+6−5
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
11} from "react-native";11} from "react-native";
12 12
13import { AppText as Text } from "../../components/AppText";13import { AppText as Text } from "../../components/AppText";
14import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
14import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule";15import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule";
15import {16import {
16 buildGhosttyThemeConfig,17 buildGhosttyThemeConfig,
@@ -53,7 +54,7 @@ function estimateGridSize(input: { · 36 unmodified lines
53}54}
54 55
55const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) {56const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) {
56 const fontSize = props.fontSize ?? 12;57 const fontSize = props.fontSize ?? MOBILE_TYPOGRAPHY.label.fontSize;
57 const inputRef = useRef<TextInput>(null);58 const inputRef = useRef<TextInput>(null);
58 const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";59 const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";
59 const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme);60 const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme);
@@ -93,7 +94,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter · 33 unmodified lines
93 <Text94 <Text
94 style={{95 style={{
95 color: theme.mutedForeground,96 color: theme.mutedForeground,
96 fontSize: 11,97 fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
97 paddingBottom: 8,98 paddingBottom: 8,
98 }}99 }}
99 >100 >
@@ -140,7 +141,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter · 40 unmodified lines
140 color: theme.foreground,141 color: theme.foreground,
141 flex: 1,142 flex: 1,
142 fontFamily: "Menlo",143 fontFamily: "Menlo",
143 fontSize: 13,144 fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
144 padding: 0,145 padding: 0,
145 }}146 }}
146 onSubmitEditing={(event) => {147 onSubmitEditing={(event) => {
@@ -165,7 +166,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter · 18 unmodified lines
165 style={{166 style={{
166 color: theme.foreground,167 color: theme.foreground,
167 fontFamily: "DMSans_700Bold",168 fontFamily: "DMSans_700Bold",
168 fontSize: 11,169 fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
169 }}170 }}
170 >171 >
171 Ctrl-C172 Ctrl-C
@@ -177,7 +178,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter · 5 unmodified lines
177});178});
178 179
179export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) {180export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) {
180 const fontSize = props.fontSize ?? 12;181 const fontSize = props.fontSize ?? MOBILE_TYPOGRAPHY.label.fontSize;
181 const keyboardInputRef = useRef<TextInput>(null);182 const keyboardInputRef = useRef<TextInput>(null);
182 const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";183 const appearanceScheme = useColorScheme() === "light" ? "light" : "dark";
183 const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme);184 const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme);
apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx+3−3
@@ -127,16 +127,16 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( · 126 unmodified lines
127 <View className="absolute inset-x-3 bottom-28 top-28 overflow-hidden rounded-[8px] border border-white/10 bg-neutral-950 shadow-2xl">127 <View className="absolute inset-x-3 bottom-28 top-28 overflow-hidden rounded-[8px] border border-white/10 bg-neutral-950 shadow-2xl">
128 <View className="flex-row items-center justify-between border-b border-white/10 px-3 py-2">128 <View className="flex-row items-center justify-between border-b border-white/10 px-3 py-2">
129 <View className="min-w-0 flex-1">129 <View className="min-w-0 flex-1">
130 <Text className="font-t3-bold text-[13px] text-neutral-100" numberOfLines={1}>130 <Text className="font-t3-bold text-sm text-neutral-100" numberOfLines={1}>
131 Terminal131 Terminal
132 </Text>132 </Text>
133 <Text className="text-[11px] text-neutral-500" numberOfLines={1}>133 <Text className="text-2xs text-neutral-500" numberOfLines={1}>
134 {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"}134 {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"}
135 </Text>135 </Text>
136 </View>136 </View>
137 <View className="flex-row items-center gap-2">137 <View className="flex-row items-center gap-2">
138 {terminal.error ? (138 {terminal.error ? (
139 <Text className="max-w-44 text-right text-[11px] text-red-300" numberOfLines={1}>139 <Text className="max-w-44 text-right text-2xs text-red-300" numberOfLines={1}>
140 {terminal.error}140 {terminal.error}
141 </Text>141 </Text>
142 ) : null}142 ) : null}
apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx+3−2
@@ -25,6 +25,7 @@ import { terminalEnvironment } from "../../state/terminal"; · 24 unmodified lines
25import { useAtomCommand } from "../../state/use-atom-command";25import { useAtomCommand } from "../../state/use-atom-command";
26import { useWorkspaceState } from "../../state/workspace";26import { useWorkspaceState } from "../../state/workspace";
27import { buildThreadTerminalNavigation } from "../../lib/routes";27import { buildThreadTerminalNavigation } from "../../lib/routes";
28import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
28import {29import {
29 useAttachedTerminalSession,30 useAttachedTerminalSession,
30 useKnownTerminalSessions,31 useKnownTerminalSessions,
@@ -920,7 +921,7 @@ export function ThreadTerminalRouteScreen() { · 889 unmodified lines
920 style={{921 style={{
921 color: terminalTheme.foreground,922 color: terminalTheme.foreground,
922 fontFamily: "DMSans_700Bold",923 fontFamily: "DMSans_700Bold",
923 fontSize: 13,924 fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
924 lineHeight: 16,925 lineHeight: 16,
925 }}926 }}
926 >927 >
@@ -932,7 +933,7 @@ export function ThreadTerminalRouteScreen() { · 5 unmodified lines
932 style={{933 style={{
933 color: terminalTheme.mutedForeground,934 color: terminalTheme.mutedForeground,
934 fontFamily: "Menlo",935 fontFamily: "Menlo",
935 fontSize: 11,936 fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
936 lineHeight: 14,937 lineHeight: 14,
937 }}938 }}
938 >939 >
apps/mobile/src/features/threads/ComposerCommandPopover.tsx+13−4
@@ -7,6 +7,7 @@ import { Pressable, ScrollView, useColorScheme, View, type ViewStyle } from "rea · 6 unmodified lines
7 7
8import { AppText as Text } from "../../components/AppText";8import { AppText as Text } from "../../components/AppText";
9import { PierreEntryIcon } from "../../components/PierreEntryIcon";9import { PierreEntryIcon } from "../../components/PierreEntryIcon";
10import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
10 11
11export type ComposerCommandItem =12export type ComposerCommandItem =
12 | {13 | {
@@ -156,14 +157,22 @@ const CommandRow = memo(function CommandRow(props: { · 143 unmodified lines
156 <SymbolView name={iconName} size={14} tintColor={iconColor} type="monochrome" />157 <SymbolView name={iconName} size={14} tintColor={iconColor} type="monochrome" />
157 ) : null}158 ) : null}
158 <Text159 <Text
159 className="text-[14px] font-t3-medium text-foreground"160 className="text-base font-t3-medium text-foreground"
160 numberOfLines={1}161 numberOfLines={1}
161 style={{ flexShrink: 0 }}162 style={{ flexShrink: 0 }}
162 >163 >
163 {props.item.label}164 {props.item.label}
164 </Text>165 </Text>
165 {props.item.description ? (166 {props.item.description ? (
166 <Text numberOfLines={1} style={{ flex: 1, minWidth: 0, fontSize: 12, color: "#a1a1aa" }}>167 <Text
168 numberOfLines={1}
169 style={{
170 flex: 1,
171 minWidth: 0,
172 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
173 color: "#a1a1aa",
174 }}
175 >
167 {props.item.description}176 {props.item.description}
168 </Text>177 </Text>
169 ) : null}178 ) : null}
@@ -182,7 +191,7 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( · 12 unmodified lines
182 {label ? (191 {label ? (
183 <View style={{ paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }}>192 <View style={{ paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }}>
184 <Text193 <Text
185 className="text-[10px] font-t3-bold text-foreground-muted"194 className="text-3xs font-t3-bold text-foreground-muted"
186 style={{ letterSpacing: 0.8, textTransform: "uppercase" }}195 style={{ letterSpacing: 0.8, textTransform: "uppercase" }}
187 >196 >
188 {label}197 {label}
@@ -206,7 +215,7 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( · 17 unmodified lines
206 </ScrollView>215 </ScrollView>
207 ) : (216 ) : (
208 <View style={{ paddingHorizontal: 14, paddingVertical: 10 }}>217 <View style={{ paddingHorizontal: 14, paddingVertical: 10 }}>
209 <Text className="text-[12px] text-foreground-tertiary">218 <Text className="text-xs text-foreground-tertiary">
210 {emptyText(props.triggerKind, props.isLoading)}219 {emptyText(props.triggerKind, props.isLoading)}
211 </Text>220 </Text>
212 </View>221 </View>
apps/mobile/src/features/threads/GitActionProgressOverlay.tsx+2−2
@@ -73,12 +73,12 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) { · 72 unmodified lines
73 73
74 <View className="flex-1 gap-0.5">74 <View className="flex-1 gap-0.5">
75 {progress.label ? (75 {progress.label ? (
76 <Text className="text-[13px] font-t3-bold text-foreground" numberOfLines={1}>76 <Text className="text-sm font-t3-bold text-foreground" numberOfLines={1}>
77 {progress.label}77 {progress.label}
78 </Text>78 </Text>
79 ) : null}79 ) : null}
80 {progress.description ? (80 {progress.description ? (
81 <Text className="text-[11px] text-foreground-muted" numberOfLines={1}>81 <Text className="text-2xs text-foreground-muted" numberOfLines={1}>
82 {progress.description}82 {progress.description}
83 </Text>83 </Text>
84 ) : null}84 ) : null}
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+2−1
@@ -30,6 +30,7 @@ import { · 29 unmodified lines
30 resolveProviderOptionDescriptors,30 resolveProviderOptionDescriptors,
31} from "../../lib/providerOptions";31} from "../../lib/providerOptions";
32import { buildThreadRoutePath } from "../../lib/routes";32import { buildThreadRoutePath } from "../../lib/routes";
33import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
33import { useProjects } from "../../state/entities";34import { useProjects } from "../../state/entities";
34import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";35import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
35import { useCreateProjectThread } from "./use-project-actions";36import { useCreateProjectThread } from "./use-project-actions";
@@ -430,7 +431,7 @@ export function NewTaskDraftScreen(props: { · 394 unmodified lines
430 onPasteImages={(uris) => void handleNativePasteImages(uris)}431 onPasteImages={(uris) => void handleNativePasteImages(uris)}
431 placeholder={`Describe a coding task in ${selectedProject.title}`}432 placeholder={`Describe a coding task in ${selectedProject.title}`}
432 style={{ flex: 1, minHeight: 0 }}433 style={{ flex: 1, minHeight: 0 }}
433 textStyle={{ fontSize: 18, lineHeight: 28 }}434 textStyle={MOBILE_TYPOGRAPHY.composer}
434 />435 />
435 </View>436 </View>
436 437
apps/mobile/src/features/threads/PendingApprovalCard.tsx+1−1
@@ -16,7 +16,7 @@ export interface PendingApprovalCardProps { · 15 unmodified lines
16export function PendingApprovalCard(props: PendingApprovalCardProps) {16export function PendingApprovalCard(props: PendingApprovalCardProps) {
17 return (17 return (
18 <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100/80 p-4 dark:border-white/6 dark:bg-neutral-900/80">18 <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100/80 p-4 dark:border-white/6 dark:bg-neutral-900/80">
19 <Text className="font-t3-bold text-[11px] uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">19 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">
20 Approval needed20 Approval needed
21 </Text>21 </Text>
22 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">22 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">
apps/mobile/src/features/threads/PendingUserInputCard.tsx+4−4
@@ -26,7 +26,7 @@ export interface PendingUserInputCardProps { · 25 unmodified lines
26export function PendingUserInputCard(props: PendingUserInputCardProps) {26export function PendingUserInputCard(props: PendingUserInputCardProps) {
27 return (27 return (
28 <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100/80 p-4 dark:border-white/6 dark:bg-neutral-900/80">28 <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100/80 p-4 dark:border-white/6 dark:bg-neutral-900/80">
29 <Text className="font-t3-bold text-[11px] uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">29 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">
30 User input needed30 User input needed
31 </Text>31 </Text>
32 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">32 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">
@@ -39,7 +39,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { · 6 unmodified lines
39 <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500 dark:text-neutral-500">39 <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500 dark:text-neutral-500">
40 {question.header}40 {question.header}
41 </Text>41 </Text>
42 <Text className="font-sans text-[15px] leading-[21px] text-neutral-950 dark:text-neutral-50">42 <Text className="font-sans text-base leading-[21px] text-neutral-950 dark:text-neutral-50">
43 {question.question}43 {question.question}
44 </Text>44 </Text>
45 <View className="flex-row flex-wrap gap-2.5">45 <View className="flex-row flex-wrap gap-2.5">
@@ -65,7 +65,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { · 19 unmodified lines
65 >65 >
66 <Text66 <Text
67 className={cn(67 className={cn(
68 "font-t3-bold text-[13px]",68 "font-t3-bold text-sm",
69 selected69 selected
70 ? "text-sky-700 dark:text-sky-300"70 ? "text-sky-700 dark:text-sky-300"
71 : "text-neutral-600 dark:text-neutral-300",71 : "text-neutral-600 dark:text-neutral-300",
@@ -83,7 +83,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { · 11 unmodified lines
83 props.onChangeCustomAnswer(props.pendingUserInput.requestId, question.id, value)83 props.onChangeCustomAnswer(props.pendingUserInput.requestId, question.id, value)
84 }84 }
85 placeholder="Or type a custom answer"85 placeholder="Or type a custom answer"
86 className="min-h-[54px] rounded-2xl border border-neutral-200 bg-white px-3.5 py-3 font-sans text-[15px] text-neutral-950 dark:border-white/8 dark:bg-neutral-950/70 dark:text-neutral-50"86 className="min-h-[54px] rounded-2xl border border-neutral-200 bg-white px-3.5 py-3 font-sans text-base text-neutral-950 dark:border-white/8 dark:bg-neutral-950/70 dark:text-neutral-50"
87 />87 />
88 </View>88 </View>
89 );89 );
apps/mobile/src/features/threads/ThreadComposer.tsx+5−6
@@ -43,6 +43,7 @@ import { ControlPill, ControlPillMenu } from "../../components/ControlPill"; · 42 unmodified lines
43import { ProviderIcon } from "../../components/ProviderIcon";43import { ProviderIcon } from "../../components/ProviderIcon";
44import type { DraftComposerImageAttachment } from "../../lib/composerImages";44import type { DraftComposerImageAttachment } from "../../lib/composerImages";
45import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";45import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
46import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
46import type { RemoteClientConnectionState } from "../../lib/connection";47import type { RemoteClientConnectionState } from "../../lib/connection";
47import {48import {
48 insertRankedSearchResult,49 insertRankedSearchResult,
@@ -189,7 +190,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( · 140 unmodified lines
189 <View className="h-2 w-2 rounded-full bg-red-500" />190 <View className="h-2 w-2 rounded-full bg-red-500" />
190 )}191 )}
191 <Text192 <Text
192 className="max-w-[260px] text-[13px] font-t3-bold leading-[17px] text-foreground"193 className="max-w-[260px] text-sm font-t3-bold leading-[17px] text-foreground"
193 numberOfLines={1}194 numberOfLines={1}
194 >195 >
195 {props.status.label}196 {props.status.label}
@@ -717,8 +718,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer · 521 unmodified lines
717 }718 }
718 }719 }
719 textStyle={{720 textStyle={{
720 fontSize: 15,721 ...MOBILE_TYPOGRAPHY.composer,
721 lineHeight: isExpanded ? 22 : 20,
722 color: foregroundColor,722 color: foregroundColor,
723 fontFamily: "DMSans_400Regular",723 fontFamily: "DMSans_400Regular",
724 }}724 }}
@@ -751,7 +751,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer · 26 unmodified lines
751 justifyContent: "center",751 justifyContent: "center",
752 }}752 }}
753 >753 >
754 <Text className="text-foreground-muted text-[11px] font-t3-bold">754 <Text className="text-foreground-muted text-2xs font-t3-bold">
755 +{props.draftAttachments.length - 3}755 +{props.draftAttachments.length - 3}
756 </Text>756 </Text>
757 </View>757 </View>
@@ -831,8 +831,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer · 73 unmodified lines
831 <Text831 <Text
832 className="text-foreground-muted"832 className="text-foreground-muted"
833 style={{833 style={{
834 fontSize: 12,834 ...MOBILE_TYPOGRAPHY.label,
835 lineHeight: 18,
836 paddingTop: 8,835 paddingTop: 8,
837 }}836 }}
838 >837 >
apps/mobile/src/features/threads/ThreadFeed.tsx+12−14
@@ -56,6 +56,7 @@ import { buildReviewParsedDiff } from "../review/reviewModel"; · 55 unmodified lines
56import { cn } from "../../lib/cn";56import { cn } from "../../lib/cn";
57import type { LayoutVariant } from "../../lib/layout";57import type { LayoutVariant } from "../../lib/layout";
58import { buildThreadFilesNavigation } from "../../lib/routes";58import { buildThreadFilesNavigation } from "../../lib/routes";
59import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "../../lib/typography";
59import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";60import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
60import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";61import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";
61import {62import {
@@ -444,8 +445,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 382 unmodified lines
444 marginRight: 5,445 marginRight: 5,
445 color: inlineTextColor,446 color: inlineTextColor,
446 fontFamily: "DMSans_400Regular",447 fontFamily: "DMSans_400Regular",
447 fontSize: 15,448 ...MOBILE_TYPOGRAPHY.body,
448 lineHeight: 22,
449 textAlign: ordered ? "right" : "center",449 textAlign: ordered ? "right" : "center",
450 }}450 }}
451 >451 >
@@ -466,7 +466,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 14 unmodified lines
466 style={{466 style={{
467 color: inlineCodeTextColor,467 color: inlineCodeTextColor,
468 fontFamily: "ui-monospace",468 fontFamily: "ui-monospace",
469 fontSize: 12,469 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
470 lineHeight: 22,470 lineHeight: 22,
471 }}471 }}
472 >472 >
@@ -503,7 +503,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 30 unmodified lines
503 style={{503 style={{
504 color: markdownBodyColor,504 color: markdownBodyColor,
505 fontFamily: "ui-monospace",505 fontFamily: "ui-monospace",
506 fontSize: 12,506 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
507 opacity: 0.7,507 opacity: 0.7,
508 textTransform: "uppercase",508 textTransform: "uppercase",
509 }}509 }}
@@ -523,7 +523,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 13 unmodified lines
523 style={{523 style={{
524 color: blockTextColor,524 color: blockTextColor,
525 fontFamily: "ui-monospace",525 fontFamily: "ui-monospace",
526 fontSize: 12,526 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
527 lineHeight: 18,527 lineHeight: 18,
528 }}528 }}
529 >529 >
@@ -603,8 +603,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 73 unmodified lines
603 skillTextColor: "#f0abfc",603 skillTextColor: "#f0abfc",
604 quoteMarkerColor: markdownUserBodyColor,604 quoteMarkerColor: markdownUserBodyColor,
605 dividerColor: markdownUserBodyColor,605 dividerColor: markdownUserBodyColor,
606 fontSize: 15,606 ...MOBILE_TYPOGRAPHY.body,
607 lineHeight: 22,
608 fontFamily: "DMSans_400Regular",607 fontFamily: "DMSans_400Regular",
609 headingFontFamily: "DMSans_700Bold",608 headingFontFamily: "DMSans_700Bold",
610 boldFontFamily: "DMSans_700Bold",609 boldFontFamily: "DMSans_700Bold",
@@ -633,8 +632,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe · 22 unmodified lines
633 skillTextColor: inlineSkillForeground,632 skillTextColor: inlineSkillForeground,
634 quoteMarkerColor: markdownBlockquoteBorder,633 quoteMarkerColor: markdownBlockquoteBorder,
635 dividerColor: markdownHrColor,634 dividerColor: markdownHrColor,
636 fontSize: 15,635 ...MOBILE_TYPOGRAPHY.body,
637 lineHeight: 22,
638 fontFamily: "DMSans_400Regular",636 fontFamily: "DMSans_400Regular",
639 headingFontFamily: "DMSans_700Bold",637 headingFontFamily: "DMSans_700Bold",
640 boldFontFamily: "DMSans_700Bold",638 boldFontFamily: "DMSans_700Bold",
@@ -821,7 +819,7 @@ function renderFeedEntry( · 180 unmodified lines
821 className="max-w-[85%] gap-2 rounded-[22px] rounded-br-[6px] px-3.5 py-2.5 opacity-60"819 className="max-w-[85%] gap-2 rounded-[22px] rounded-br-[6px] px-3.5 py-2.5 opacity-60"
822 style={{ backgroundColor: userBubbleColor }}820 style={{ backgroundColor: userBubbleColor }}
823 >821 >
824 <Text className="font-sans text-[15px] leading-[22px] text-white">822 <Text className="font-sans text-base leading-[22px] text-white">
825 {entry.queuedMessage.text}823 {entry.queuedMessage.text}
826 </Text>824 </Text>
827 {entry.queuedMessage.attachments.length > 0 ? (825 {entry.queuedMessage.attachments.length > 0 ? (
@@ -995,7 +993,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { · 167 unmodified lines
995 </View>993 </View>
996 <View className="min-w-0 flex-1">994 <View className="min-w-0 flex-1">
997 <Text995 <Text
998 className="font-mono text-[12px] leading-[16px]"996 className="font-mono text-xs leading-[16px]"
999 numberOfLines={1}997 numberOfLines={1}
1000 style={{ color: props.colors.text }}998 style={{ color: props.colors.text }}
1001 >999 >
@@ -1040,8 +1038,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { · 38 unmodified lines
1040 style={{1038 style={{
1041 color: props.colors.text,1039 color: props.colors.text,
1042 fontFamily: "ui-monospace",1040 fontFamily: "ui-monospace",
1043 fontSize: 12,1041 fontSize: MOBILE_CODE_SURFACE.fontSize,
1044 lineHeight: 18,1042 lineHeight: MOBILE_CODE_SURFACE.rowHeight,
1045 }}1043 }}
1046 >1044 >
1047 {props.comment.diff.trim()}1045 {props.comment.diff.trim()}
@@ -1052,7 +1050,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { · 4 unmodified lines
1052 <View className="border-t px-3 py-3" style={{ borderColor: props.colors.border }}>1050 <View className="border-t px-3 py-3" style={{ borderColor: props.colors.border }}>
1053 <Text1051 <Text
1054 selectable1052 selectable
1055 className="text-[15px] leading-[21px]"1053 className="text-base leading-[21px]"
1056 style={{ color: props.colors.text }}1054 style={{ color: props.colors.text }}
1057 >1055 >
1058 {props.comment.text}1056 {props.comment.text}
apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx+5−7
@@ -152,7 +152,7 @@ export function ThreadNavigationDrawer(props: { · 151 unmodified lines
152 ]}152 ]}
153 >153 >
154 <View className="flex-row items-center justify-between px-4 pb-5">154 <View className="flex-row items-center justify-between px-4 pb-5">
155 <Text className="text-[26px] font-t3-bold">Threads</Text>155 <Text className="text-2xl font-t3-bold">Threads</Text>
156 <Pressable156 <Pressable
157 onPress={() => {157 onPress={() => {
158 props.onClose();158 props.onClose();
@@ -224,7 +224,7 @@ function ThreadNavigationDrawerContent(props: { · 65 unmodified lines
224 {groupedThreads.map((group) => (224 {groupedThreads.map((group) => (
225 <View key={group.key} className="gap-3">225 <View key={group.key} className="gap-3">
226 <Text226 <Text
227 className="px-1 text-[15px] font-t3-bold text-foreground-muted"227 className="px-1 text-base font-t3-bold text-foreground-muted"
228 style={{ letterSpacing: -0.2 }}228 style={{ letterSpacing: -0.2 }}
229 >229 >
230 {group.title}230 {group.title}
@@ -233,9 +233,7 @@ function ThreadNavigationDrawerContent(props: { · 2 unmodified lines
233 <View className="overflow-hidden rounded-[22px] bg-card">233 <View className="overflow-hidden rounded-[22px] bg-card">
234 {group.threads.length === 0 ? (234 {group.threads.length === 0 ? (
235 <View className="px-4 py-4">235 <View className="px-4 py-4">
236 <Text className="text-[14px] font-medium text-foreground-tertiary">236 <Text className="text-sm font-medium text-foreground-tertiary">No threads yet</Text>
237 No threads yet
238 </Text>
239 </View>237 </View>
240 ) : (238 ) : (
241 group.threads.map((thread, index) => {239 group.threads.map((thread, index) => {
@@ -260,11 +258,11 @@ function ThreadNavigationDrawerContent(props: { · 18 unmodified lines
260 >258 >
261 <View className="flex-row items-start justify-between gap-3">259 <View className="flex-row items-start justify-between gap-3">
262 <View className="flex-1 gap-1">260 <View className="flex-1 gap-1">
263 <Text className="text-[16px] font-t3-bold" numberOfLines={1}>261 <Text className="text-base font-t3-bold" numberOfLines={1}>
264 {thread.title}262 {thread.title}
265 </Text>263 </Text>
266 <Text264 <Text
267 className="text-[13px] font-medium text-foreground-muted"265 className="text-sm font-medium text-foreground-muted"
268 numberOfLines={1}266 numberOfLines={1}
269 >267 >
270 {relativeTime(thread.updatedAt ?? thread.createdAt)}268 {relativeTime(thread.updatedAt ?? thread.createdAt)}
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+3−2
@@ -20,6 +20,7 @@ import { EmptyState } from "../../components/EmptyState"; · 19 unmodified lines
20import { LoadingScreen } from "../../components/LoadingScreen";20import { LoadingScreen } from "../../components/LoadingScreen";
21import { buildThreadRoutePath, buildThreadTerminalNavigation } from "../../lib/routes";21import { buildThreadRoutePath, buildThreadTerminalNavigation } from "../../lib/routes";
22import { scopedThreadKey } from "../../lib/scopedEntities";22import { scopedThreadKey } from "../../lib/scopedEntities";
23import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
23import { connectionTone } from "../connection/connectionTone";24import { connectionTone } from "../connection/connectionTone";
24 25
25import {26import {
@@ -390,7 +391,7 @@ export function ThreadRouteScreen() { · 364 unmodified lines
390 numberOfLines={1}391 numberOfLines={1}
391 style={{392 style={{
392 fontFamily: "DMSans_700Bold",393 fontFamily: "DMSans_700Bold",
393 fontSize: 18,394 fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
394 fontWeight: "900",395 fontWeight: "900",
395 color: foregroundColor,396 color: foregroundColor,
396 letterSpacing: -0.4,397 letterSpacing: -0.4,
@@ -402,7 +403,7 @@ export function ThreadRouteScreen() { · 5 unmodified lines
402 numberOfLines={1}403 numberOfLines={1}
403 style={{404 style={{
404 fontFamily: "DMSans_700Bold",405 fontFamily: "DMSans_700Bold",
405 fontSize: 12,406 fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
406 fontWeight: "700",407 fontWeight: "700",
407 color: secondaryFg,408 color: secondaryFg,
408 letterSpacing: 0.3,409 letterSpacing: 0.3,
apps/mobile/src/features/threads/git/GitBranchesSheet.tsx+10−12
@@ -69,7 +69,7 @@ export function GitBranchesSheet() { · 68 unmodified lines
69 >69 >
70 <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">70 <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">
71 <Text71 <Text
72 className="text-foreground-secondary text-[11px] font-t3-bold uppercase"72 className="text-foreground-secondary text-2xs font-t3-bold uppercase"
73 style={{ letterSpacing: 1 }}73 style={{ letterSpacing: 1 }}
74 >74 >
75 New branch75 New branch
@@ -78,7 +78,7 @@ export function GitBranchesSheet() { · 2 unmodified lines
78 value={newBranchName}78 value={newBranchName}
79 onChangeText={setNewBranchName}79 onChangeText={setNewBranchName}
80 placeholder="feature/mobile-polish"80 placeholder="feature/mobile-polish"
81 className="rounded-[18px] px-3.5 py-3 font-sans text-[15px]"81 className="rounded-[18px] px-3.5 py-3 font-sans text-base"
82 style={{82 style={{
83 borderWidth: 1,83 borderWidth: 1,
84 borderColor: inputBorderColor,84 borderColor: inputBorderColor,
@@ -104,7 +104,7 @@ export function GitBranchesSheet() { · 19 unmodified lines
104 104
105 <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">105 <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">
106 <Text106 <Text
107 className="text-foreground-secondary text-[11px] font-t3-bold uppercase"107 className="text-foreground-secondary text-2xs font-t3-bold uppercase"
108 style={{ letterSpacing: 1 }}108 style={{ letterSpacing: 1 }}
109 >109 >
110 New worktree110 New worktree
@@ -113,7 +113,7 @@ export function GitBranchesSheet() { · 2 unmodified lines
113 value={worktreeBaseBranch}113 value={worktreeBaseBranch}
114 onChangeText={setWorktreeBaseBranch}114 onChangeText={setWorktreeBaseBranch}
115 placeholder="main"115 placeholder="main"
116 className="rounded-[18px] px-3.5 py-3 font-sans text-[15px]"116 className="rounded-[18px] px-3.5 py-3 font-sans text-base"
117 style={{117 style={{
118 borderWidth: 1,118 borderWidth: 1,
119 borderColor: inputBorderColor,119 borderColor: inputBorderColor,
@@ -125,7 +125,7 @@ export function GitBranchesSheet() { · 5 unmodified lines
125 value={worktreeBranchName}125 value={worktreeBranchName}
126 onChangeText={setWorktreeBranchName}126 onChangeText={setWorktreeBranchName}
127 placeholder="feature/mobile-thread"127 placeholder="feature/mobile-thread"
128 className="rounded-[18px] px-3.5 py-3 font-sans text-[15px]"128 className="rounded-[18px] px-3.5 py-3 font-sans text-base"
129 style={{129 style={{
130 borderWidth: 1,130 borderWidth: 1,
131 borderColor: inputBorderColor,131 borderColor: inputBorderColor,
@@ -154,18 +154,16 @@ export function GitBranchesSheet() { · 22 unmodified lines
154 154
155 <View className="gap-2">155 <View className="gap-2">
156 <Text156 <Text
157 className="text-foreground-secondary text-[11px] font-t3-bold uppercase"157 className="text-foreground-secondary text-2xs font-t3-bold uppercase"
158 style={{ letterSpacing: 1 }}158 style={{ letterSpacing: 1 }}
159 >159 >
160 Existing branches160 Existing branches
161 </Text>161 </Text>
162 {branchesLoading ? (162 {branchesLoading ? (
163 <Text className="text-foreground-secondary text-[13px] font-medium">163 <Text className="text-foreground-secondary text-sm font-medium">Loading branches...</Text>
164 Loading branches...
165 </Text>
166 ) : null}164 ) : null}
167 {!branchesLoading && availableBranches.length === 0 ? (165 {!branchesLoading && availableBranches.length === 0 ? (
168 <Text className="text-foreground-secondary text-[13px] font-medium">166 <Text className="text-foreground-secondary text-sm font-medium">
169 No local branches found.167 No local branches found.
170 </Text>168 </Text>
171 ) : null}169 ) : null}
@@ -195,8 +193,8 @@ export function GitBranchesSheet() { · 23 unmodified lines
195 }}193 }}
196 >194 >
197 <View className="absolute inset-0 rounded-[18px] bg-card" />195 <View className="absolute inset-0 rounded-[18px] bg-card" />
198 <Text className="text-foreground text-[15px] font-t3-bold">{branch.name}</Text>196 <Text className="text-foreground text-base font-t3-bold">{branch.name}</Text>
199 <Text className="text-foreground-secondary text-[12px] font-medium">{subtitle}</Text>197 <Text className="text-foreground-secondary text-xs font-medium">{subtitle}</Text>
200 </Pressable>198 </Pressable>
201 );199 );
202 })}200 })}
apps/mobile/src/features/threads/git/GitCommitSheet.tsx+18−18
@@ -79,14 +79,14 @@ export function GitCommitSheet() { · 78 unmodified lines
79 >79 >
80 <View className="gap-3 rounded-[22px] border border-border bg-card px-4 py-4">80 <View className="gap-3 rounded-[22px] border border-border bg-card px-4 py-4">
81 <View className="flex-row items-center justify-between gap-3">81 <View className="flex-row items-center justify-between gap-3">
82 <Text className="text-foreground-muted text-[13px] font-medium">Branch</Text>82 <Text className="text-foreground-muted text-sm font-medium">Branch</Text>
83 <Text className="text-foreground text-[15px] font-t3-bold">83 <Text className="text-foreground text-base font-t3-bold">
84 {gitStatus.data?.refName ?? "(detached HEAD)"}84 {gitStatus.data?.refName ?? "(detached HEAD)"}
85 </Text>85 </Text>
86 </View>86 </View>
87 {isDefaultRef ? (87 {isDefaultRef ? (
88 <Text88 <Text
89 className="text-[12px] leading-[18px]"89 className="text-xs leading-[18px]"
90 style={{ color: isDarkMode ? "#fbbf24" : "#b45309" }}90 style={{ color: isDarkMode ? "#fbbf24" : "#b45309" }}
91 >91 >
92 Warning: this is the default branch.92 Warning: this is the default branch.
@@ -97,8 +97,8 @@ export function GitCommitSheet() { · 4 unmodified lines
97 <View className="gap-3 rounded-[22px] border border-border bg-card px-4 py-4">97 <View className="gap-3 rounded-[22px] border border-border bg-card px-4 py-4">
98 <View className="flex-row items-center justify-between gap-3">98 <View className="flex-row items-center justify-between gap-3">
99 <View className="gap-1">99 <View className="gap-1">
100 <Text className="text-foreground text-[16px] font-t3-bold">Files</Text>100 <Text className="text-foreground text-base font-t3-bold">Files</Text>
101 <Text className="text-foreground-muted text-[12px] leading-[18px]">101 <Text className="text-foreground-muted text-xs leading-[18px]">
102 {selectedFiles.length} selected · +{selectedInsertions} / -{selectedDeletions}102 {selectedFiles.length} selected · +{selectedInsertions} / -{selectedDeletions}
103 </Text>103 </Text>
104 </View>104 </View>
@@ -108,14 +108,14 @@ export function GitCommitSheet() { · 3 unmodified lines
108 className="bg-subtle rounded-full px-3 py-2"108 className="bg-subtle rounded-full px-3 py-2"
109 onPress={() => setExcludedFiles(new Set())}109 onPress={() => setExcludedFiles(new Set())}
110 >110 >
111 <Text className="text-foreground text-[11px] font-t3-bold uppercase">Reset</Text>111 <Text className="text-foreground text-2xs font-t3-bold uppercase">Reset</Text>
112 </Pressable>112 </Pressable>
113 ) : null}113 ) : null}
114 <Pressable114 <Pressable
115 className="bg-subtle rounded-full px-3 py-2"115 className="bg-subtle rounded-full px-3 py-2"
116 onPress={() => setIsEditingFiles((current) => !current)}116 onPress={() => setIsEditingFiles((current) => !current)}
117 >117 >
118 <Text className="text-foreground text-[11px] font-t3-bold uppercase">118 <Text className="text-foreground text-2xs font-t3-bold uppercase">
119 {isEditingFiles ? "Done" : "Edit"}119 {isEditingFiles ? "Done" : "Edit"}
120 </Text>120 </Text>
121 </Pressable>121 </Pressable>
@@ -123,26 +123,26 @@ export function GitCommitSheet() { · 1 unmodified line
123 </View>123 </View>
124 124
125 {allFiles.length === 0 ? (125 {allFiles.length === 0 ? (
126 <Text className="text-foreground-secondary text-[13px] leading-[19px]">126 <Text className="text-foreground-secondary text-sm leading-[19px]">
127 No changed files are available to commit.127 No changed files are available to commit.
128 </Text>128 </Text>
129 ) : !isEditingFiles ? (129 ) : !isEditingFiles ? (
130 <View className="gap-2">130 <View className="gap-2">
131 {selectedFilePreview.map((file) => (131 {selectedFilePreview.map((file) => (
132 <View key={file.path} className="flex-row items-center justify-between gap-3">132 <View key={file.path} className="flex-row items-center justify-between gap-3">
133 <Text className="text-foreground flex-1 text-[13px] font-medium" numberOfLines={1}>133 <Text className="text-foreground flex-1 text-sm font-medium" numberOfLines={1}>
134 {file.path}134 {file.path}
135 </Text>135 </Text>
136 <Text className="text-[12px] font-t3-bold" style={{ color: "#10b981" }}>136 <Text className="text-xs font-t3-bold" style={{ color: "#10b981" }}>
137 +{file.insertions}137 +{file.insertions}
138 </Text>138 </Text>
139 <Text className="text-[12px] font-t3-bold" style={{ color: "#f43f5e" }}>139 <Text className="text-xs font-t3-bold" style={{ color: "#f43f5e" }}>
140 -{file.deletions}140 -{file.deletions}
141 </Text>141 </Text>
142 </View>142 </View>
143 ))}143 ))}
144 {selectedFiles.length > selectedFilePreview.length ? (144 {selectedFiles.length > selectedFilePreview.length ? (
145 <Text className="text-foreground-muted text-[12px] leading-[17px]">145 <Text className="text-foreground-muted text-xs leading-[17px]">
146 +{selectedFiles.length - selectedFilePreview.length} more files146 +{selectedFiles.length - selectedFilePreview.length} more files
147 </Text>147 </Text>
148 ) : null}148 ) : null}
@@ -177,21 +177,21 @@ export function GitCommitSheet() { · 28 unmodified lines
177 <View className="flex-1 gap-1">177 <View className="flex-1 gap-1">
178 <Text178 <Text
179 selectable179 selectable
180 className={`text-[13px] font-t3-bold ${included ? "text-foreground" : "text-foreground-muted"}`}180 className={`text-sm font-t3-bold ${included ? "text-foreground" : "text-foreground-muted"}`}
181 >181 >
182 {file.path}182 {file.path}
183 </Text>183 </Text>
184 {!included ? (184 {!included ? (
185 <Text className="text-foreground-muted text-[11px] leading-[16px]">185 <Text className="text-foreground-muted text-2xs leading-[16px]">
186 Excluded from this commit186 Excluded from this commit
187 </Text>187 </Text>
188 ) : null}188 ) : null}
189 </View>189 </View>
190 <View className="items-end gap-1">190 <View className="items-end gap-1">
191 <Text className="text-[12px] font-t3-bold" style={{ color: "#10b981" }}>191 <Text className="text-xs font-t3-bold" style={{ color: "#10b981" }}>
192 +{file.insertions}192 +{file.insertions}
193 </Text>193 </Text>
194 <Text className="text-[12px] font-t3-bold" style={{ color: "#f43f5e" }}>194 <Text className="text-xs font-t3-bold" style={{ color: "#f43f5e" }}>
195 -{file.deletions}195 -{file.deletions}
196 </Text>196 </Text>
197 </View>197 </View>
@@ -204,14 +204,14 @@ export function GitCommitSheet() { · 6 unmodified lines
204 </View>204 </View>
205 205
206 <View className="gap-2">206 <View className="gap-2">
207 <Text className="text-foreground text-[13px] font-t3-bold">Commit message</Text>207 <Text className="text-foreground text-sm font-t3-bold">Commit message</Text>
208 <TextInput208 <TextInput
209 multiline209 multiline
210 value={dialogCommitMessage}210 value={dialogCommitMessage}
211 onChangeText={setDialogCommitMessage}211 onChangeText={setDialogCommitMessage}
212 placeholder="Leave empty to auto-generate"212 placeholder="Leave empty to auto-generate"
213 textAlignVertical="top"213 textAlignVertical="top"
214 className="min-h-[128px] rounded-[20px] px-4 py-3.5 font-sans text-[15px]"214 className="min-h-[128px] rounded-[20px] px-4 py-3.5 font-sans text-base"
215 style={{215 style={{
216 minHeight: 128,216 minHeight: 128,
217 borderWidth: 1,217 borderWidth: 1,
apps/mobile/src/features/threads/git/GitConfirmSheet.tsx+3−3
@@ -90,15 +90,15 @@ export function GitConfirmSheet() { · 89 unmodified lines
90 90
91 <View className="items-center gap-1 px-5 pb-3 pt-4">91 <View className="items-center gap-1 px-5 pb-3 pt-4">
92 <Text92 <Text
93 className="text-[12px] font-t3-bold uppercase text-foreground-muted"93 className="text-xs font-t3-bold uppercase text-foreground-muted"
94 style={{ letterSpacing: 1 }}94 style={{ letterSpacing: 1 }}
95 >95 >
96 Confirm96 Confirm
97 </Text>97 </Text>
98 <Text className="text-center text-[28px] font-t3-bold">98 <Text className="text-center text-3xl font-t3-bold">
99 {copy?.title ?? "Run action on default branch?"}99 {copy?.title ?? "Run action on default branch?"}
100 </Text>100 </Text>
101 <Text className="text-center text-foreground-secondary text-[13px] font-medium leading-[19px]">101 <Text className="text-center text-foreground-secondary text-sm font-medium leading-[19px]">
102 {copy?.description ?? "Choose how to continue."}102 {copy?.description ?? "Choose how to continue."}
103 </Text>103 </Text>
104 </View>104 </View>
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+3−3
@@ -175,13 +175,13 @@ export function GitOverviewSheet() { · 174 unmodified lines
175 />175 />
176 </Pressable>176 </Pressable>
177 <Text177 <Text
178 className="text-[12px] font-t3-bold uppercase text-foreground-muted"178 className="text-xs font-t3-bold uppercase text-foreground-muted"
179 style={{ letterSpacing: 1 }}179 style={{ letterSpacing: 1 }}
180 >180 >
181 Branch181 Branch
182 </Text>182 </Text>
183 <Text className="text-[28px] font-t3-bold">{currentBranchLabel}</Text>183 <Text className="text-3xl font-t3-bold">{currentBranchLabel}</Text>
184 <Text className="text-foreground-secondary text-[13px] font-medium leading-[19px]">184 <Text className="text-foreground-secondary text-sm font-medium leading-[19px]">
185 {statusSummary(gitStatus.data)}185 {statusSummary(gitStatus.data)}
186 </Text>186 </Text>
187 </View>187 </View>
apps/mobile/src/features/threads/git/gitSheetComponents.tsx+5−5
@@ -56,7 +56,7 @@ export function SheetActionButton(props: { · 55 unmodified lines
56 >56 >
57 <SymbolView name={props.icon} size={16} tintColor={colors.textColor} type="monochrome" />57 <SymbolView name={props.icon} size={16} tintColor={colors.textColor} type="monochrome" />
58 <Text58 <Text
59 className="text-[12px] font-t3-bold uppercase"59 className="text-xs font-t3-bold uppercase"
60 style={{ color: colors.textColor, letterSpacing: 0.9 }}60 style={{ color: colors.textColor, letterSpacing: 0.9 }}
61 >61 >
62 {props.label}62 {props.label}
@@ -69,12 +69,12 @@ export function MetaCard(props: { readonly label: string; readonly value: string · 6 unmodified lines
69 return (69 return (
70 <View className="rounded-[18px] border border-border bg-card px-4 py-3">70 <View className="rounded-[18px] border border-border bg-card px-4 py-3">
71 <Text71 <Text
72 className="text-foreground-muted text-[11px] font-t3-bold uppercase"72 className="text-foreground-muted text-2xs font-t3-bold uppercase"
73 style={{ letterSpacing: 0.9 }}73 style={{ letterSpacing: 0.9 }}
74 >74 >
75 {props.label}75 {props.label}
76 </Text>76 </Text>
77 <Text selectable className="text-foreground text-[13px] font-medium" numberOfLines={1}>77 <Text selectable className="text-foreground text-sm font-medium" numberOfLines={1}>
78 {props.value}78 {props.value}
79 </Text>79 </Text>
80 </View>80 </View>
@@ -102,9 +102,9 @@ export function SheetListRow(props: { · 21 unmodified lines
102 <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" />102 <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" />
103 </View>103 </View>
104 <View className="flex-1 gap-0.5">104 <View className="flex-1 gap-0.5">
105 <Text className="text-foreground text-[16px] font-t3-bold">{props.title}</Text>105 <Text className="text-foreground text-base font-t3-bold">{props.title}</Text>
106 {props.subtitle ? (106 {props.subtitle ? (
107 <Text className="text-foreground-muted text-[12px] leading-[17px]">{props.subtitle}</Text>107 <Text className="text-foreground-muted text-xs leading-[17px]">{props.subtitle}</Text>
108 ) : null}108 ) : null}
109 </View>109 </View>
110 <SymbolView name="chevron.right" size={13} tintColor={iconSubtleColor} type="monochrome" />110 <SymbolView name="chevron.right" size={13} tintColor={iconSubtleColor} type="monochrome" />
apps/mobile/src/features/threads/thread-work-log.tsx+3−3
@@ -98,7 +98,7 @@ export function ThreadWorkLog(props: { · 97 unmodified lines
98 return (98 return (
99 <View className="-mx-1 mb-3 px-1 py-0.5">99 <View className="-mx-1 mb-3 px-1 py-0.5">
100 {!onlyToolRows ? (100 {!onlyToolRows ? (
101 <Text className="px-0.5 pb-0.5 font-t3-medium text-[11px] text-foreground-muted opacity-60">101 <Text className="px-0.5 pb-0.5 font-t3-medium text-2xs text-foreground-muted opacity-60">
102 work log102 work log
103 </Text>103 </Text>
104 ) : null}104 ) : null}
@@ -164,7 +164,7 @@ export function ThreadWorkLog(props: { · 59 unmodified lines
164 164
165 <View className="shrink-0 flex-row items-center gap-px">165 <View className="shrink-0 flex-row items-center gap-px">
166 {props.copiedRowId === row.id ? (166 {props.copiedRowId === row.id ? (
167 <Text className="pr-1 font-t3-medium text-[10px] text-emerald-600 dark:text-emerald-400">167 <Text className="pr-1 font-t3-medium text-3xs text-emerald-600 dark:text-emerald-400">
168 Copied168 Copied
169 </Text>169 </Text>
170 ) : null}170 ) : null}
@@ -209,7 +209,7 @@ export function ThreadWorkLog(props: { · 38 unmodified lines
209 >209 >
210 <Text210 <Text
211 selectable211 selectable
212 className="text-[11px] leading-[17px] text-foreground-muted"212 className="text-2xs leading-[17px] text-foreground-muted"
213 style={{ fontFamily: "ui-monospace" }}213 style={{ fontFamily: "ui-monospace" }}
214 >214 >
215 {row.fullDetail}215 {row.fullDetail}
apps/mobile/src/lib/typography.test.ts+23−0
@@ -0,0 +1,23 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "./typography";
4
5describe("mobile typography", () => {
6 it("uses the intentional compact mobile font scale", () => {
7 expect(Object.values(MOBILE_TYPOGRAPHY).map(({ fontSize }) => fontSize)).toEqual([
8 10, 11, 12, 13, 14, 15, 17, 20, 24, 28,
9 ]);
10 });
11
12 it("uses a compact shared style for editable composer text", () => {
13 expect(MOBILE_TYPOGRAPHY.composer).toEqual({ fontSize: 14, lineHeight: 20 });
14 });
15
16 it("uses caption-sized code with a compact readable row height", () => {
17 expect(MOBILE_CODE_SURFACE).toMatchObject({
18 fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
19 lineNumberFontSize: MOBILE_TYPOGRAPHY.micro.fontSize,
20 rowHeight: 20,
21 });
22 });
23});
apps/mobile/src/lib/typography.ts+22−0
@@ -0,0 +1,22 @@
1export const MOBILE_TYPOGRAPHY = {
2 micro: { fontSize: 10, lineHeight: 13 },
3 caption: { fontSize: 11, lineHeight: 15 },
4 label: { fontSize: 12, lineHeight: 16 },
5 footnote: { fontSize: 13, lineHeight: 18 },
6 composer: { fontSize: 14, lineHeight: 20 },
7 body: { fontSize: 15, lineHeight: 22 },
8 headline: { fontSize: 17, lineHeight: 22 },
9 title: { fontSize: 20, lineHeight: 26 },
10 largeTitle: { fontSize: 24, lineHeight: 30 },
11 display: { fontSize: 28, lineHeight: 34 },
12} as const;
13
14/** Shared geometry for dense, horizontally scrolling code surfaces. */
15export const MOBILE_CODE_SURFACE = {
16 rowHeight: 20,
17 gutterWidth: 46,
18 codePadding: 7,
19 textVerticalInset: 2,
20 fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
21 lineNumberFontSize: MOBILE_TYPOGRAPHY.micro.fontSize,
22} as const;
apps/mobile/src/native/T3ComposerEditor.ios.tsx+9−2
@@ -6,6 +6,7 @@ import { Image, StyleSheet } from "react-native"; · 5 unmodified lines
6 6
7import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";7import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
8import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";8import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
9import { MOBILE_TYPOGRAPHY } from "../lib/typography";
9import { useThemeColor } from "../lib/useThemeColor";10import { useThemeColor } from "../lib/useThemeColor";
10import type { ComposerEditorProps, ComposerEditorSelection } from "./T3ComposerEditor.types";11import type { ComposerEditorProps, ComposerEditorSelection } from "./T3ComposerEditor.types";
11 12
@@ -150,9 +151,15 @@ export function ComposerEditor({ · 138 unmodified lines
150 ? resolvedTextStyle.fontFamily151 ? resolvedTextStyle.fontFamily
151 : "DMSans_400Regular"152 : "DMSans_400Regular"
152 }153 }
153 fontSize={typeof resolvedTextStyle.fontSize === "number" ? resolvedTextStyle.fontSize : 15}154 fontSize={
155 typeof resolvedTextStyle.fontSize === "number"
156 ? resolvedTextStyle.fontSize
157 : MOBILE_TYPOGRAPHY.composer.fontSize
158 }
154 lineHeight={159 lineHeight={
155 typeof resolvedTextStyle.lineHeight === "number" ? resolvedTextStyle.lineHeight : 22160 typeof resolvedTextStyle.lineHeight === "number"
161 ? resolvedTextStyle.lineHeight
162 : MOBILE_TYPOGRAPHY.composer.lineHeight
156 }163 }
157 contentInsetVertical={contentInsetVertical}164 contentInsetVertical={contentInsetVertical}
158 editable={props.editable ?? true}165 editable={props.editable ?? true}
apps/mobile/src/native/T3ComposerEditor.tsx+2−2
@@ -2,6 +2,7 @@ import { TextInputWrapper } from "expo-paste-input"; · 1 unmodified line
2import { useImperativeHandle, useRef } from "react";2import { useImperativeHandle, useRef } from "react";
3import { TextInput, type TextInput as RNTextInput } from "react-native";3import { TextInput, type TextInput as RNTextInput } from "react-native";
4 4
5import { MOBILE_TYPOGRAPHY } from "../lib/typography";
5import { useThemeColor } from "../lib/useThemeColor";6import { useThemeColor } from "../lib/useThemeColor";
6import { useNativePaste } from "../lib/useNativePaste";7import { useNativePaste } from "../lib/useNativePaste";
7import type { ComposerEditorProps } from "./T3ComposerEditor.types";8import type { ComposerEditorProps } from "./T3ComposerEditor.types";
@@ -47,8 +48,7 @@ export function ComposerEditor({ · 39 unmodified lines
47 minHeight: 0,48 minHeight: 0,
48 color: foregroundColor,49 color: foregroundColor,
49 fontFamily: "DMSans_400Regular",50 fontFamily: "DMSans_400Regular",
50 fontSize: 15,51 ...MOBILE_TYPOGRAPHY.composer,
51 lineHeight: 22,
52 paddingVertical: contentInsetVertical,52 paddingVertical: contentInsetVertical,
53 },53 },
54 textStyle,54 textStyle,
apps/server/src/auth/EnvironmentAuth.test.ts+10−9
@@ -4,27 +4,26 @@ import { expect, it } from "@effect/vitest"; · 3 unmodified lines
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6 6
7import type { ServerConfigShape } from "../config.ts";7import * as ServerConfig from "../config.ts";
8import { ServerConfig } from "../config.ts";
9import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
10import * as PairingGrantStore from "./PairingGrantStore.ts";9import * as PairingGrantStore from "./PairingGrantStore.ts";
11import * as EnvironmentAuth from "./EnvironmentAuth.ts";10import * as EnvironmentAuth from "./EnvironmentAuth.ts";
12 11
13import * as ServerSecretStore from "./ServerSecretStore.ts";12import * as ServerSecretStore from "./ServerSecretStore.ts";
14 13
15const makeServerConfigLayer = (overrides?: Partial<ServerConfigShape>) =>14const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));24 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));
26 25
27const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) =>26const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
28 EnvironmentAuth.layer.pipe(27 EnvironmentAuth.layer.pipe(
29 Layer.provide(SqlitePersistenceMemory),28 Layer.provide(SqlitePersistenceMemory),
30 Layer.provide(ServerSecretStore.layer),29 Layer.provide(ServerSecretStore.layer),
@@ -33,13 +32,15 @@ const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) => · 2 unmodified lines
33 32
34const makeCookieRequest = (33const makeCookieRequest = (
35 sessionToken: string,34 sessionToken: string,
36): Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0] =>35): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[0] =>
37 ({36 ({
38 cookies: {37 cookies: {
39 t3_session: sessionToken,38 t3_session: sessionToken,
40 },39 },
41 headers: {},40 headers: {},
42 }) as unknown as Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0];41 }) as unknown as Parameters<
42 EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]
43 >[0];
43 44
44const requestMetadata = {45const requestMetadata = {
45 deviceType: "desktop" as const,46 deviceType: "desktop" as const,
apps/server/src/auth/EnvironmentAuthAdmin.test.ts+6−7
@@ -3,31 +3,30 @@ import { expect, it } from "@effect/vitest"; · 2 unmodified lines
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5 5
6import type { ServerConfigShape } from "../config.ts";6import * as ServerConfig from "../config.ts";
7import { ServerConfig } from "../config.ts";
8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";7import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
9import * as EnvironmentAuth from "./EnvironmentAuth.ts";8import * as EnvironmentAuth from "./EnvironmentAuth.ts";
10import * as ServerSecretStore from "./ServerSecretStore.ts";9import * as ServerSecretStore from "./ServerSecretStore.ts";
11import * as SessionStore from "./SessionStore.ts";10import * as SessionStore from "./SessionStore.ts";
12 11
13const makeServerConfigLayer = (12const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
15) =>14) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(24 ).pipe(
26 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })),25 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })),
27 );26 );
28 27
29const makeEnvironmentAuthLayer = (28const makeEnvironmentAuthLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
31) =>30) =>
32 EnvironmentAuth.layer.pipe(31 EnvironmentAuth.layer.pipe(
33 Layer.provideMerge(ServerSecretStore.layer),32 Layer.provideMerge(ServerSecretStore.layer),
apps/server/src/auth/EnvironmentAuthPolicy.test.ts+7−6
@@ -3,21 +3,22 @@ import { expect, it } from "@effect/vitest"; · 2 unmodified lines
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5 5
6import type { ServerConfigShape } from "../config.ts";6import * as ServerConfig from "../config.ts";
7import { ServerConfig } from "../config.ts";
8import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";7import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";
9 8
10const makeEnvironmentAuthPolicyLayer = (overrides?: Partial<ServerConfigShape>) =>9const makeEnvironmentAuthPolicyLayer = (
10 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,
11) =>
11 EnvironmentAuthPolicy.layer.pipe(12 EnvironmentAuthPolicy.layer.pipe(
12 Layer.provide(13 Layer.provide(
13 Layer.effect(14 Layer.effect(
14 ServerConfig,15 ServerConfig.ServerConfig,
15 Effect.gen(function* () {16 Effect.gen(function* () {
16 const config = yield* ServerConfig;17 const config = yield* ServerConfig.ServerConfig;
17 return {18 return {
18 ...config,19 ...config,
19 ...overrides,20 ...overrides,
20 } satisfies ServerConfigShape;21 } satisfies ServerConfig.ServerConfig["Service"];
21 }),22 }),
22 ).pipe(23 ).pipe(
23 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-policy-test-" })),24 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-policy-test-" })),
apps/server/src/auth/PairingGrantStore.test.ts+6−7
@@ -5,29 +5,28 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";6import * as TestClock from "effect/testing/TestClock";
7 7
8import type { ServerConfigShape } from "../config.ts";8import * as ServerConfig from "../config.ts";
9import { ServerConfig } from "../config.ts";
10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";9import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
11import * as PairingGrantStore from "./PairingGrantStore.ts";10import * as PairingGrantStore from "./PairingGrantStore.ts";
12 11
13const makeServerConfigLayer = (12const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
15) =>14) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(24 ).pipe(
26 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),25 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),
27 );26 );
28 27
29const makePairingGrantStoreLayer = (28const makePairingGrantStoreLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
31) =>30) =>
32 PairingGrantStore.layer.pipe(31 PairingGrantStore.layer.pipe(
33 Layer.provide(SqlitePersistenceMemory),32 Layer.provide(SqlitePersistenceMemory),
apps/server/src/auth/SessionStore.test.ts+8−9
@@ -5,30 +5,29 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";6import * as TestClock from "effect/testing/TestClock";
7 7
8import type { ServerConfigShape } from "../config.ts";8import * as ServerConfig from "../config.ts";
9import { ServerConfig } from "../config.ts";
10import { PersistenceSqlError } from "../persistence/Errors.ts";9import { PersistenceSqlError } from "../persistence/Errors.ts";
11import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
12import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts";11import * as AuthSessions from "../persistence/Services/AuthSessions.ts";
13import * as SessionStore from "./SessionStore.ts";12import * as SessionStore from "./SessionStore.ts";
14import * as ServerSecretStore from "./ServerSecretStore.ts";13import * as ServerSecretStore from "./ServerSecretStore.ts";
15 14
16const makeServerConfigLayer = (15const makeServerConfigLayer = (
17 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,16 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
18) =>17) =>
19 Layer.effect(18 Layer.effect(
20 ServerConfig,19 ServerConfig.ServerConfig,
21 Effect.gen(function* () {20 Effect.gen(function* () {
22 const config = yield* ServerConfig;21 const config = yield* ServerConfig.ServerConfig;
23 return {22 return {
24 ...config,23 ...config,
25 ...overrides,24 ...overrides,
26 } satisfies ServerConfigShape;25 } satisfies ServerConfig.ServerConfig["Service"];
27 }),26 }),
28 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));27 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));
29 28
30const makeSessionStoreLayer = (29const makeSessionStoreLayer = (
31 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,30 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
32) =>31) =>
33 SessionStore.layer.pipe(32 SessionStore.layer.pipe(
34 Layer.provide(SqlitePersistenceMemory),33 Layer.provide(SqlitePersistenceMemory),
@@ -41,7 +40,7 @@ const repositoryFailure = new PersistenceSqlError({ · 6 unmodified lines
41 detail: "sqlite is unavailable",40 detail: "sqlite is unavailable",
42});41});
43 42
44const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessionRepository, {43const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessionRepository, {
45 create: () => Effect.void,44 create: () => Effect.void,
46 getById: () => Effect.fail(repositoryFailure),45 getById: () => Effect.fail(repositoryFailure),
47 listActive: () => Effect.succeed([]),46 listActive: () => Effect.succeed([]),
apps/server/src/bin.test.ts+11−14
@@ -20,8 +20,8 @@ import * as TestConsole from "effect/testing/TestConsole"; · 19 unmodified lines
20import { Command } from "effect/unstable/cli";20import { Command } from "effect/unstable/cli";
21 21
22import { cli, makeCli } from "./bin.ts";22import { cli, makeCli } from "./bin.ts";
23import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts";23import * as ServerConfig from "./config.ts";
24import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";24import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
25import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";25import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
26import { orchestrationHttpApiLayer } from "./orchestration/http.ts";26import { orchestrationHttpApiLayer } from "./orchestration/http.ts";
27import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";27import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
@@ -57,7 +57,7 @@ const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) => · 29 unmodified lines
57 57
58const makeCliTestServerConfig = (baseDir: string) =>58const makeCliTestServerConfig = (baseDir: string) =>
59 Effect.gen(function* () {59 Effect.gen(function* () {
60 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);60 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
61 return {61 return {
62 logLevel: "Info",62 logLevel: "Info",
63 traceMinLevel: "Info",63 traceMinLevel: "Info",
@@ -84,26 +84,23 @@ const makeCliTestServerConfig = (baseDir: string) => · 20 unmodified lines
84 logWebSocketEvents: false,84 logWebSocketEvents: false,
85 tailscaleServeEnabled: false,85 tailscaleServeEnabled: false,
86 tailscaleServePort: 443,86 tailscaleServePort: 443,
87 } satisfies ServerConfigShape;87 } satisfies ServerConfig.ServerConfig["Service"];
88 });88 });
89 89
90const makeProjectPersistenceLayer = (config: ServerConfigShape) =>90const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) =>
91 Layer.mergeAll(91 Layer.mergeAll(
92 OrchestrationLayerLive.pipe(92 OrchestrationLayerLive.pipe(
93 Layer.provideMerge(RepositoryIdentityResolverLive),93 Layer.provideMerge(RepositoryIdentityResolverLive),
94 Layer.provideMerge(SqlitePersistenceLayerLive),94 Layer.provideMerge(SqlitePersistenceLayerLive),
95 ),95 ),
96 WorkspacePathsLive,96 WorkspacePathsLive,
97 ).pipe(97 ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)));
98 Layer.provideMerge(NodeServices.layer),
99 Layer.provide(Layer.succeed(ServerConfig, config)),
100 );
101 98
102const readPersistedSnapshot = (baseDir: string) =>99const readPersistedSnapshot = (baseDir: string) =>
103 Effect.gen(function* () {100 Effect.gen(function* () {
104 const config = yield* makeCliTestServerConfig(baseDir);101 const config = yield* makeCliTestServerConfig(baseDir);
105 return yield* Effect.gen(function* () {102 return yield* Effect.gen(function* () {
106 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;103 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
107 return yield* projectionSnapshotQuery.getSnapshot();104 return yield* projectionSnapshotQuery.getSnapshot();
108 }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));105 }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));
109 });106 });
@@ -133,7 +130,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef · 23 unmodified lines
133 }),130 }),
134 ),131 ),
135 Layer.provideMerge(NodeServices.layer),132 Layer.provideMerge(NodeServices.layer),
136 Layer.provide(Layer.succeed(ServerConfig, config)),133 Layer.provide(ServerConfig.layer(config)),
137 );134 );
138 135
139 return yield* Effect.scoped(136 return yield* Effect.scoped(
@@ -238,7 +235,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { · 98 unmodified lines
238 it.effect("logs in to headless connect without enabling access", () =>235 it.effect("logs in to headless connect without enabling access", () =>
239 Effect.gen(function* () {236 Effect.gen(function* () {
240 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-login-test-"));237 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-login-test-"));
241 const { secretsDir } = yield* deriveServerPaths(baseDir, undefined);238 const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
242 mkdirSync(secretsDir, { recursive: true });239 mkdirSync(secretsDir, { recursive: true });
243 writeFileSync(240 writeFileSync(
244 join(secretsDir, "cloud-cli-oauth-token.bin"),241 join(secretsDir, "cloud-cli-oauth-token.bin"),
@@ -282,7 +279,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { · 37 unmodified lines
282 it.effect("logs out of headless connect and removes the stored CLI authorization", () =>279 it.effect("logs out of headless connect and removes the stored CLI authorization", () =>
283 Effect.gen(function* () {280 Effect.gen(function* () {
284 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-logout-test-"));281 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-logout-test-"));
285 const { secretsDir } = yield* deriveServerPaths(baseDir, undefined);282 const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
286 const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin");283 const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin");
287 mkdirSync(secretsDir, { recursive: true });284 mkdirSync(secretsDir, { recursive: true });
288 writeFileSync(tokenPath, "invalid persisted token");285 writeFileSync(tokenPath, "invalid persisted token");
@@ -461,7 +458,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { · 172 unmodified lines
461 "--base-dir",458 "--base-dir",
462 baseDir,459 baseDir,
463 ]);460 ]);
464 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;461 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
465 const readModel = yield* projectionSnapshotQuery.getSnapshot();462 const readModel = yield* projectionSnapshotQuery.getSnapshot();
466 const addedProject = readModel.projects.find(463 const addedProject = readModel.projects.find(
467 (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,464 (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,
apps/server/src/cli/auth.ts+3−3
@@ -18,7 +18,7 @@ import { · 17 unmodified lines
18 formatPairingCredentialList,18 formatPairingCredentialList,
19 formatSessionList,19 formatSessionList,
20} from "../cliAuthFormat.ts";20} from "../cliAuthFormat.ts";
21import { ServerConfig } from "../config.ts";21import * as ServerConfig from "../config.ts";
22import {22import {
23 authLocationFlags,23 authLocationFlags,
24 type CliAuthLocationFlags,24 type CliAuthLocationFlags,
@@ -28,7 +28,7 @@ import { · 3 unmodified lines
28 28
29const runWithEnvironmentAuth = <A, E>(29const runWithEnvironmentAuth = <A, E>(
30 flags: CliAuthLocationFlags,30 flags: CliAuthLocationFlags,
31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuthShape) => Effect.Effect<A, E>,31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"]) => Effect.Effect<A, E>,
32 options?: {32 options?: {
33 readonly quietLogs?: boolean;33 readonly quietLogs?: boolean;
34 },34 },
@@ -43,7 +43,7 @@ const runWithEnvironmentAuth = <A, E>( · 8 unmodified lines
43 }).pipe(43 }).pipe(
44 Effect.provide(44 Effect.provide(
45 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(45 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(
46 Layer.provide(Layer.succeed(ServerConfig, config)),46 Layer.provide(ServerConfig.layer(config)),
47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
48 ),48 ),
49 ),49 ),
apps/server/src/cli/config.ts+12−20
@@ -14,18 +14,10 @@ import * as SchemaTransformation from "effect/SchemaTransformation"; · 13 unmodified lines
14import { Argument, Flag } from "effect/unstable/cli";14import { Argument, Flag } from "effect/unstable/cli";
15 15
16import { readBootstrapEnvelope } from "../bootstrap.ts";16import { readBootstrapEnvelope } from "../bootstrap.ts";
17import {17import * as ServerConfig from "../config.ts";
18 DEFAULT_PORT,
19 deriveServerPaths,
20 ensureServerDirectories,
21 resolveStaticDir,
22 RuntimeMode,
23 type ServerConfigShape,
24 type StartupPresentation,
25} from "../config.ts";
26import { expandHomePath, resolveBaseDir } from "../os-jank.ts";18import { expandHomePath, resolveBaseDir } from "../os-jank.ts";
27 19
28export const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe(20export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).pipe(
29 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),21 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
30 Flag.optional,22 Flag.optional,
31);23);
@@ -104,7 +96,7 @@ const EnvServerConfig = Config.all({ · 72 unmodified lines
104 Config.withDefault(10_000),96 Config.withDefault(10_000),
105 ),97 ),
106 otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),98 otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
107 mode: Config.schema(RuntimeMode, "T3CODE_MODE").pipe(99 mode: Config.schema(ServerConfig.RuntimeMode, "T3CODE_MODE").pipe(
108 Config.option,100 Config.option,
109 Config.map(Option.getOrUndefined),101 Config.map(Option.getOrUndefined),
110 ),102 ),
@@ -139,7 +131,7 @@ const EnvServerConfig = Config.all({ · 28 unmodified lines
139});131});
140 132
141export interface CliServerFlags {133export interface CliServerFlags {
142 readonly mode: Option.Option<RuntimeMode>;134 readonly mode: Option.Option<ServerConfig.RuntimeMode>;
143 readonly port: Option.Option<number>;135 readonly port: Option.Option<number>;
144 readonly host: Option.Option<string>;136 readonly host: Option.Option<string>;
145 readonly baseDir: Option.Option<string>;137 readonly baseDir: Option.Option<string>;
@@ -208,7 +200,7 @@ export const resolveServerConfig = ( · 62 unmodified lines
208 flags: CliServerFlags,200 flags: CliServerFlags,
209 cliLogLevel: Option.Option<LogLevel.LogLevel>,201 cliLogLevel: Option.Option<LogLevel.LogLevel>,
210 options?: {202 options?: {
211 readonly startupPresentation?: StartupPresentation;203 readonly startupPresentation?: ServerConfig.StartupPresentation;
212 readonly forceAutoBootstrapProjectFromCwd?: boolean;204 readonly forceAutoBootstrapProjectFromCwd?: boolean;
213 },205 },
214) =>206) =>
@@ -238,7 +230,7 @@ export const resolveServerConfig = ( · 23 unmodified lines
238 : Option.none();230 : Option.none();
239 const bootstrap = Option.getOrUndefined(bootstrapEnvelope);231 const bootstrap = Option.getOrUndefined(bootstrapEnvelope);
240 232
241 const mode: RuntimeMode = Option.getOrElse(233 const mode: ServerConfig.RuntimeMode = Option.getOrElse(
242 resolveOptionPrecedence(234 resolveOptionPrecedence(
243 normalizedFlags.mode,235 normalizedFlags.mode,
244 Option.fromUndefinedOr(env.mode),236 Option.fromUndefinedOr(env.mode),
@@ -257,9 +249,9 @@ export const resolveServerConfig = ( · 12 unmodified lines
257 onSome: (value) => Effect.succeed(value),249 onSome: (value) => Effect.succeed(value),
258 onNone: () => {250 onNone: () => {
259 if (mode === "desktop") {251 if (mode === "desktop") {
260 return Effect.succeed(DEFAULT_PORT);252 return Effect.succeed(ServerConfig.DEFAULT_PORT);
261 }253 }
262 return findAvailablePort(DEFAULT_PORT);254 return findAvailablePort(ServerConfig.DEFAULT_PORT);
263 },255 },
264 },256 },
265 );257 );
@@ -279,8 +271,8 @@ export const resolveServerConfig = ( · 13 unmodified lines
279 const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());271 const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());
280 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));272 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));
281 yield* fs.makeDirectory(cwd, { recursive: true });273 yield* fs.makeDirectory(cwd, { recursive: true });
282 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);274 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
283 yield* ensureServerDirectories(derivedPaths);275 yield* ServerConfig.ensureServerDirectories(derivedPaths);
284 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(276 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(
285 derivedPaths.settingsPath,277 derivedPaths.settingsPath,
286 );278 );
@@ -330,7 +322,7 @@ export const resolveServerConfig = ( · 43 unmodified lines
330 ),322 ),
331 () => 443,323 () => 443,
332 );324 );
333 const staticDir = devUrl ? undefined : yield* resolveStaticDir();325 const staticDir = devUrl ? undefined : yield* ServerConfig.resolveStaticDir();
334 const host = Option.getOrElse(326 const host = Option.getOrElse(
335 resolveOptionPrecedence(327 resolveOptionPrecedence(
336 normalizedFlags.host,328 normalizedFlags.host,
@@ -341,7 +333,7 @@ export const resolveServerConfig = ( · 4 unmodified lines
341 );333 );
342 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);334 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);
343 335
344 const config: ServerConfigShape = {336 const config: ServerConfig.ServerConfig["Service"] = {
345 logLevel,337 logLevel,
346 traceMinLevel: env.traceMinLevel,338 traceMinLevel: env.traceMinLevel,
347 traceTimingEnabled: env.traceTimingEnabled,339 traceTimingEnabled: env.traceTimingEnabled,
apps/server/src/cli/connect.ts+9−9
@@ -31,9 +31,9 @@ import * as CliTokenManager from "../cloud/CliTokenManager.ts"; · 30 unmodified lines
31import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts";31import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts";
32import { relayUrlConfig } from "../cloud/publicConfig.ts";32import { relayUrlConfig } from "../cloud/publicConfig.ts";
33import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";33import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";
34import { ServerConfig } from "../config.ts";34import * as ServerConfig from "../config.ts";
35import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts";35import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts";
36import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts";36import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts";
37import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";37import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
38import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";38import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
39 39
@@ -145,7 +145,7 @@ const reportRelayClientInstallProgress = (event: RelayClientInstallProgressEvent · 105 unmodified lines
145 145
146export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")(146export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")(
147 function* <ConfirmError, ConfirmContext>(147 function* <ConfirmError, ConfirmContext>(
148 relayClient: RelayClient.RelayClientShape,148 relayClient: RelayClient.RelayClient["Service"],
149 confirmInstall: (version: string) => Effect.Effect<boolean, ConfirmError, ConfirmContext>,149 confirmInstall: (version: string) => Effect.Effect<boolean, ConfirmError, ConfirmContext>,
150 reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect<void>,150 reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect<void>,
151 ) {151 ) {
@@ -164,7 +164,7 @@ export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_clie · 12 unmodified lines
164);164);
165 165
166const withCloudCliSessionToken = <A, E, R>(166const withCloudCliSessionToken = <A, E, R>(
167 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,167 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
168 run: (token: string) => Effect.Effect<A, E, R>,168 run: (token: string) => Effect.Effect<A, E, R>,
169) =>169) =>
170 Effect.acquireUseRelease(170 Effect.acquireUseRelease(
@@ -183,7 +183,7 @@ type LiveCloudActionResult = · 12 unmodified lines
183 | { readonly status: "failed"; readonly cause: unknown };183 | { readonly status: "failed"; readonly cause: unknown };
184 184
185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {
186 const config = yield* ServerConfig;186 const config = yield* ServerConfig.ServerConfig;
187 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);187 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
188 if (Option.isNone(runtimeState)) {188 if (Option.isNone(runtimeState)) {
189 return { status: "not-running" } satisfies LiveCloudActionResult;189 return { status: "not-running" } satisfies LiveCloudActionResult;
@@ -219,7 +219,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f · 29 unmodified lines
219 return { status: "not-authenticated" } satisfies RelayUnlinkResult;219 return { status: "not-authenticated" } satisfies RelayUnlinkResult;
220 }220 }
221 221
222 const environment = yield* ServerEnvironment;222 const environment = yield* ServerEnvironment.ServerEnvironment;
223 const environmentId = yield* environment.getEnvironmentId;223 const environmentId = yield* environment.getEnvironmentId;
224 const relayUrl = yield* relayUrlConfig;224 const relayUrl = yield* relayUrlConfig;
225 const httpClient = yield* HttpClient.HttpClient;225 const httpClient = yield* HttpClient.HttpClient;
@@ -285,8 +285,8 @@ const runCloudCommand = <A, E>( · 59 unmodified lines
285 | FileSystem.FileSystem285 | FileSystem.FileSystem
286 | HttpClient.HttpClient286 | HttpClient.HttpClient
287 | Prompt.Environment287 | Prompt.Environment
288 | ServerConfig288 | ServerConfig.ServerConfig
289 | ServerEnvironment289 | ServerEnvironment.ServerEnvironment
290 >,290 >,
291 options?: {291 options?: {
292 readonly quietLogs?: boolean;292 readonly quietLogs?: boolean;
@@ -305,7 +305,7 @@ const runCloudCommand = <A, E>( · 12 unmodified lines
305 headlessRelayClientTracingLayer,305 headlessRelayClientTracingLayer,
306 ).pipe(306 ).pipe(
307 Layer.provideMerge(FetchHttpClient.layer),307 Layer.provideMerge(FetchHttpClient.layer),
308 Layer.provideMerge(Layer.succeed(ServerConfig, config)),308 Layer.provideMerge(ServerConfig.layer(config)),
309 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),309 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
310 );310 );
311 return yield* run.pipe(Effect.provide(runtimeLayer));311 return yield* run.pipe(Effect.provide(runtimeLayer));
apps/server/src/cli/project.ts+21−14
@@ -26,19 +26,19 @@ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; · 25 unmodified lines
26 26
27import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";27import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
28 28
29import { ServerConfig, type ServerConfigShape } from "../config.ts";29import * as ServerConfig from "../config.ts";
30import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";30import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts";
31import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";31import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
32import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";32import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";
33import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";33import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";
34import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts";34import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts";
35import { getAutoBootstrapDefaultModelSelection } from "../serverRuntimeStartup.ts";35import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts";
36import {36import {
37 clearPersistedServerRuntimeState,37 clearPersistedServerRuntimeState,
38 readPersistedServerRuntimeState,38 readPersistedServerRuntimeState,
39} from "../serverRuntimeState.ts";39} from "../serverRuntimeState.ts";
40import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts";40import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts";
41import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts";41import * as WorkspacePaths from "../workspace/Services/WorkspacePaths.ts";
42import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts";42import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
43 43
44type ProjectMutationTarget = {44type ProjectMutationTarget = {
@@ -78,7 +78,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll( · 33 unmodified lines
78const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);78const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);
79const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);79const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
80const withProjectCliSessionToken = <A, E, R>(80const withProjectCliSessionToken = <A, E, R>(
81 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,81 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
82 run: (token: string) => Effect.Effect<A, E, R>,82 run: (token: string) => Effect.Effect<A, E, R>,
83) =>83) =>
84 Effect.acquireUseRelease(84 Effect.acquireUseRelease(
@@ -123,7 +123,7 @@ const makeLiveServerClient = (origin: string) => · 38 unmodified lines
123const normalizeWorkspaceRootForProjectCommand = Effect.fn(123const normalizeWorkspaceRootForProjectCommand = Effect.fn(
124 "normalizeWorkspaceRootForProjectCommand",124 "normalizeWorkspaceRootForProjectCommand",
125)(function* (workspaceRoot: string) {125)(function* (workspaceRoot: string) {
126 const workspacePaths = yield* WorkspacePaths;126 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
127 return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);127 return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);
128});128});
129 129
@@ -211,12 +211,15 @@ const dispatchLiveOrchestrationCommand = ( · 81 unmodified lines
211 }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest));211 }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest));
212 212
213const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {213const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {
214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
215 return yield* projectionSnapshotQuery.getSnapshot();215 return yield* projectionSnapshotQuery.getSnapshot();
216});216});
217 217
218const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(218const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(
219 function* (environmentAuth: EnvironmentAuth.EnvironmentAuthShape, config: ServerConfigShape) {219 function* (
220 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
221 config: ServerConfig.ServerConfig["Service"],
222 ) {
220 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);223 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
221 if (Option.isNone(runtimeState)) {224 if (Option.isNone(runtimeState)) {
222 return Option.none<{ readonly origin: string }>();225 return Option.none<{ readonly origin: string }>();
@@ -251,7 +254,11 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( · 28 unmodified lines
251 }) => Effect.Effect<254 }) => Effect.Effect<
252 string,255 string,
253 Error,256 Error,
254 Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths257 | Crypto.Crypto
258 | FileSystem.FileSystem
259 | HttpClient.HttpClient
260 | Path.Path
261 | WorkspacePaths.WorkspacePaths
255 >,262 >,
256) {263) {
257 const logLevel = yield* GlobalFlag.LogLevel;264 const logLevel = yield* GlobalFlag.LogLevel;
@@ -278,13 +285,13 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( · 20 unmodified lines
278 }285 }
279 286
280 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(287 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(
281 Layer.provide(Layer.succeed(ServerConfig, config)),288 Layer.provide(ServerConfig.layer(config)),
282 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),289 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
283 );290 );
284 291
285 return yield* Effect.gen(function* () {292 return yield* Effect.gen(function* () {
286 const snapshot = yield* getOfflineSnapshot();293 const snapshot = yield* getOfflineSnapshot();
287 const orchestrationEngine = yield* OrchestrationEngineService;294 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
288 const output = yield* run({295 const output = yield* run({
289 snapshot,296 snapshot,
290 dispatch: (command) => orchestrationEngine.dispatch(command),297 dispatch: (command) => orchestrationEngine.dispatch(command),
@@ -296,7 +303,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( · 5 unmodified lines
296 Effect.provide(303 Effect.provide(
297 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe(304 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe(
298 Layer.provideMerge(FetchHttpClient.layer),305 Layer.provideMerge(FetchHttpClient.layer),
299 Layer.provide(Layer.succeed(ServerConfig, config)),306 Layer.provide(ServerConfig.layer(config)),
300 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),307 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
301 ),308 ),
302 ),309 ),
@@ -341,7 +348,7 @@ const projectAddCommand = Command.make("add", { · 38 unmodified lines
341 projectId,348 projectId,
342 title,349 title,
343 workspaceRoot,350 workspaceRoot,
344 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),351 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
345 createdAt: DateTime.formatIso(yield* DateTime.now),352 createdAt: DateTime.formatIso(yield* DateTime.now),
346 });353 });
347 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;354 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;
apps/server/src/config.ts+83−76
@@ -6,13 +6,13 @@ · 5 unmodified lines
6 *6 *
7 * @module ServerConfig7 * @module ServerConfig
8 */8 */
9import * as Context from "effect/Context";
9import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";11import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
12import * as LogLevel from "effect/LogLevel";13import * as LogLevel from "effect/LogLevel";
13import * as Path from "effect/Path";14import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
15import * as Context from "effect/Context";
16 16
17export const DEFAULT_PORT = 3773;17export const DEFAULT_PORT = 3773;
18 18
@@ -46,38 +46,51 @@ export interface ServerDerivedPaths { · 27 unmodified lines
46}46}
47 47
48/**48/**
49 * ServerConfigShape - Process/runtime configuration required by the server.49 * ServerConfig - Service tag for server runtime configuration.
50 */50 */
51export interface ServerConfigShape extends ServerDerivedPaths {51export class ServerConfig extends Context.Service<
52 readonly logLevel: LogLevel.LogLevel;52 ServerConfig,
53 readonly traceMinLevel: LogLevel.LogLevel;53 ServerDerivedPaths & {
54 readonly traceTimingEnabled: boolean;54 readonly logLevel: LogLevel.LogLevel;
55 readonly traceBatchWindowMs: number;55 readonly traceMinLevel: LogLevel.LogLevel;
56 readonly traceMaxBytes: number;56 readonly traceTimingEnabled: boolean;
57 readonly traceMaxFiles: number;57 readonly traceBatchWindowMs: number;
58 readonly otlpTracesUrl: string | undefined;58 readonly traceMaxBytes: number;
59 readonly otlpMetricsUrl: string | undefined;59 readonly traceMaxFiles: number;
60 readonly otlpExportIntervalMs: number;60 readonly otlpTracesUrl: string | undefined;
61 readonly otlpServiceName: string;61 readonly otlpMetricsUrl: string | undefined;
62 readonly mode: RuntimeMode;62 readonly otlpExportIntervalMs: number;
63 readonly port: number;63 readonly otlpServiceName: string;
64 readonly host: string | undefined;64 readonly mode: RuntimeMode;
65 readonly cwd: string;65 readonly port: number;
66 readonly baseDir: string;66 readonly host: string | undefined;
67 readonly staticDir: string | undefined;67 readonly cwd: string;
68 readonly devUrl: URL | undefined;68 readonly baseDir: string;
69 readonly noBrowser: boolean;69 readonly staticDir: string | undefined;
70 readonly startupPresentation: StartupPresentation;70 readonly devUrl: URL | undefined;
71 readonly desktopBootstrapToken: string | undefined;71 readonly noBrowser: boolean;
72 readonly autoBootstrapProjectFromCwd: boolean;72 readonly startupPresentation: StartupPresentation;
73 readonly logWebSocketEvents: boolean;73 readonly desktopBootstrapToken: string | undefined;
74 readonly tailscaleServeEnabled: boolean;74 readonly autoBootstrapProjectFromCwd: boolean;
75 readonly tailscaleServePort: number;75 readonly logWebSocketEvents: boolean;
76 readonly tailscaleServeEnabled: boolean;
77 readonly tailscaleServePort: number;
78 }
79>()("t3/config/ServerConfig") {
80 /** @deprecated Import and use `layerTest` from this module. */
81 static readonly layerTest = (
82 cwd: string,
83 baseDirOrPrefix: string | { readonly prefix: string },
84 ) => layerTest(cwd, baseDirOrPrefix);
76}85}
77 86
87export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
88
89export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
90
78export const deriveServerPaths = Effect.fn(function* (91export const deriveServerPaths = Effect.fn(function* (
79 baseDir: ServerConfigShape["baseDir"],92 baseDir: ServerConfig["Service"]["baseDir"],
80 devUrl: ServerConfigShape["devUrl"],93 devUrl: ServerConfig["Service"]["devUrl"],
81): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {94): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
82 const { join } = yield* Path.Path;95 const { join } = yield* Path.Path;
83 const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata");96 const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata");
@@ -129,56 +142,50 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server · 45 unmodified lines
129 );142 );
130});143});
131 144
132/**145const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
133 * ServerConfig - Service tag for server runtime configuration.146 cwd: string,
134 */147 baseDirOrPrefix: string | { readonly prefix: string },
135export class ServerConfig extends Context.Service<ServerConfig, ServerConfigShape>()(
136 "t3/config/ServerConfig",
137) {148) {
138 static readonly layerTest = (cwd: string, baseDirOrPrefix: string | { prefix: string }) =>149 const devUrl = undefined;
139 Layer.effect(150 const fs = yield* FileSystem.FileSystem;
140 ServerConfig,151 const baseDir =
141 Effect.gen(function* () {152 typeof baseDirOrPrefix === "string"
142 const devUrl = undefined;153 ? baseDirOrPrefix
154 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });
155 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
156 yield* ensureServerDirectories(derivedPaths);
143 157
144 const fs = yield* FileSystem.FileSystem;158 return ServerConfig.of({
145 const baseDir =159 logLevel: "Error",
146 typeof baseDirOrPrefix === "string"160 traceMinLevel: "Info",
147 ? baseDirOrPrefix161 traceTimingEnabled: true,
148 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });162 traceBatchWindowMs: 200,
149 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);163 traceMaxBytes: 10 * 1024 * 1024,
150 yield* ensureServerDirectories(derivedPaths);164 traceMaxFiles: 10,
165 otlpTracesUrl: undefined,
166 otlpMetricsUrl: undefined,
167 otlpExportIntervalMs: 10_000,
168 otlpServiceName: "t3-server",
169 cwd,
170 baseDir,
171 ...derivedPaths,
172 mode: "web",
173 autoBootstrapProjectFromCwd: false,
174 logWebSocketEvents: false,
175 tailscaleServeEnabled: false,
176 tailscaleServePort: 443,
177 port: 0,
178 host: undefined,
179 desktopBootstrapToken: undefined,
180 staticDir: undefined,
181 devUrl,
182 noBrowser: false,
183 startupPresentation: "browser",
184 });
185});
151 186
152 return {187export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) =>
153 logLevel: "Error",188 Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix));
154 traceMinLevel: "Info",
155 traceTimingEnabled: true,
156 traceBatchWindowMs: 200,
157 traceMaxBytes: 10 * 1024 * 1024,
158 traceMaxFiles: 10,
159 otlpTracesUrl: undefined,
160 otlpMetricsUrl: undefined,
161 otlpExportIntervalMs: 10_000,
162 otlpServiceName: "t3-server",
163 cwd,
164 baseDir,
165 ...derivedPaths,
166 mode: "web",
167 autoBootstrapProjectFromCwd: false,
168 logWebSocketEvents: false,
169 tailscaleServeEnabled: false,
170 tailscaleServePort: 443,
171 port: 0,
172 host: undefined,
173 desktopBootstrapToken: undefined,
174 staticDir: undefined,
175 devUrl,
176 noBrowser: false,
177 startupPresentation: "browser",
178 } satisfies ServerConfigShape;
179 }),
180 );
181}
182 189
183export const resolveStaticDir = Effect.fn(function* () {190export const resolveStaticDir = Effect.fn(function* () {
184 const { join, resolve } = yield* Path.Path;191 const { join, resolve } = yield* Path.Path;
apps/server/src/environment/Layers/ServerEnvironment.test.ts+8−10
@@ -8,15 +8,15 @@ import * as FileSystem from "effect/FileSystem"; · 7 unmodified lines
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
10 10
11import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";11import * as ServerConfig from "../../config.ts";
12import { ServerEnvironment } from "../Services/ServerEnvironment.ts";12import * as ServerEnvironment from "../Services/ServerEnvironment.ts";
13import { ServerEnvironmentLive } from "./ServerEnvironment.ts";13import { ServerEnvironmentLive } from "./ServerEnvironment.ts";
14 14
15const makeServerEnvironmentLayer = (baseDir: string) =>15const makeServerEnvironmentLayer = (baseDir: string) =>
16 ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));16 ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
17 17
18const makeServerConfig = Effect.fn(function* (baseDir: string) {18const makeServerConfig = Effect.fn(function* (baseDir: string) {
19 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);19 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
20 20
21 return {21 return {
22 ...derivedPaths,22 ...derivedPaths,
@@ -44,7 +44,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) { · 21 unmodified lines
44 devUrl: undefined,44 devUrl: undefined,
45 noBrowser: false,45 noBrowser: false,
46 startupPresentation: "browser",46 startupPresentation: "browser",
47 } satisfies ServerConfigShape;47 } satisfies ServerConfig.ServerConfig["Service"];
48});48});
49 49
50it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {50it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
@@ -56,11 +56,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { · 5 unmodified lines
56 });56 });
57 57
58 const first = yield* Effect.gen(function* () {58 const first = yield* Effect.gen(function* () {
59 const serverEnvironment = yield* ServerEnvironment;59 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
60 return yield* serverEnvironment.getDescriptor;60 return yield* serverEnvironment.getDescriptor;
61 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));61 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
62 const second = yield* Effect.gen(function* () {62 const second = yield* Effect.gen(function* () {
63 const serverEnvironment = yield* ServerEnvironment;63 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
64 return yield* serverEnvironment.getDescriptor;64 return yield* serverEnvironment.getDescriptor;
65 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));65 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
66 66
@@ -109,14 +109,12 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { · 42 unmodified lines
109 });109 });
110 110
111 const exit = yield* Effect.gen(function* () {111 const exit = yield* Effect.gen(function* () {
112 const serverEnvironment = yield* ServerEnvironment;112 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
113 return yield* serverEnvironment.getDescriptor;113 return yield* serverEnvironment.getDescriptor;
114 }).pipe(114 }).pipe(
115 Effect.provide(115 Effect.provide(
116 ServerEnvironmentLive.pipe(116 ServerEnvironmentLive.pipe(
117 Layer.provide(117 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
118 Layer.merge(Layer.succeed(ServerConfig, serverConfig), failingFileSystemLayer),
119 ),
120 ),118 ),
121 ),119 ),
122 Effect.exit,120 Effect.exit,
apps/server/src/keybindings.test.ts+48−55
@@ -9,28 +9,21 @@ import * as Layer from "effect/Layer"; · 8 unmodified lines
9import * as Logger from "effect/Logger";9import * as Logger from "effect/Logger";
10import * as Path from "effect/Path";10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
12import { ServerConfig } from "./config.ts";12import * as ServerConfig from "./config.ts";
13 13import * as Keybindings from "./keybindings.ts";
14import {
15 DEFAULT_KEYBINDINGS,
16 Keybindings,
17 KeybindingsLive,
18 ResolvedKeybindingFromConfig,
19 compileResolvedKeybindingRule,
20 compileResolvedKeybindingsConfig,
21 parseKeybindingShortcut,
22} from "./keybindings.ts";
23import { KeybindingsConfigError } from "@t3tools/contracts";14import { KeybindingsConfigError } from "@t3tools/contracts";
24 15
25const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);16const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
26const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);17const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
27const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);18const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);
28const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig);19const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(
20 Keybindings.ResolvedKeybindingFromConfig,
21);
29const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(22const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
30 ResolvedKeybindingFromConfig,23 Keybindings.ResolvedKeybindingFromConfig,
31);24);
32const makeKeybindingsLayer = () => {25const makeKeybindingsLayer = () => {
33 return KeybindingsLive.pipe(26 return Keybindings.layer.pipe(
34 Layer.provideMerge(27 Layer.provideMerge(
35 Layer.fresh(28 Layer.fresh(
36 ServerConfig.layerTest(process.cwd(), {29 ServerConfig.layerTest(process.cwd(), {
@@ -66,7 +59,7 @@ const readKeybindingsConfig = (configPath: string) => · 29 unmodified lines
66it.layer(NodeServices.layer)("keybindings", (it) => {59it.layer(NodeServices.layer)("keybindings", (it) => {
67 it.effect("parses shortcuts including plus key", () =>60 it.effect("parses shortcuts including plus key", () =>
68 Effect.sync(() => {61 Effect.sync(() => {
69 assert.deepEqual(parseKeybindingShortcut("mod+j"), {62 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), {
70 key: "j",63 key: "j",
71 metaKey: false,64 metaKey: false,
72 ctrlKey: false,65 ctrlKey: false,
@@ -74,7 +67,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 1 unmodified line
74 altKey: false,67 altKey: false,
75 modKey: true,68 modKey: true,
76 });69 });
77 assert.deepEqual(parseKeybindingShortcut("mod++"), {70 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), {
78 key: "+",71 key: "+",
79 metaKey: false,72 metaKey: false,
80 ctrlKey: false,73 ctrlKey: false,
@@ -87,7 +80,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 6 unmodified lines
87 80
88 it.effect("compiles valid rule with parsed when AST", () =>81 it.effect("compiles valid rule with parsed when AST", () =>
89 Effect.sync(() => {82 Effect.sync(() => {
90 const compiled = compileResolvedKeybindingRule({83 const compiled = Keybindings.compileResolvedKeybindingRule({
91 key: "mod+d",84 key: "mod+d",
92 command: "terminal.split",85 command: "terminal.split",
93 when: "terminalOpen && !terminalFocus",86 when: "terminalOpen && !terminalFocus",
@@ -137,14 +130,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 43 unmodified lines
137 it.effect("rejects invalid rules", () =>130 it.effect("rejects invalid rules", () =>
138 Effect.sync(() => {131 Effect.sync(() => {
139 assert.isNull(132 assert.isNull(
140 compileResolvedKeybindingRule({133 Keybindings.compileResolvedKeybindingRule({
141 key: "mod+shift+d+o",134 key: "mod+shift+d+o",
142 command: "terminal.new",135 command: "terminal.new",
143 }),136 }),
144 );137 );
145 138
146 assert.isNull(139 assert.isNull(
147 compileResolvedKeybindingRule({140 Keybindings.compileResolvedKeybindingRule({
148 key: "mod+d",141 key: "mod+d",
149 command: "terminal.split",142 command: "terminal.split",
150 when: "terminalFocus && (",143 when: "terminalFocus && (",
@@ -152,7 +145,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 1 unmodified line
152 );145 );
153 146
154 assert.isNull(147 assert.isNull(
155 compileResolvedKeybindingRule({148 Keybindings.compileResolvedKeybindingRule({
156 key: "mod+d",149 key: "mod+d",
157 command: "terminal.split",150 command: "terminal.split",
158 when: `${"!".repeat(300)}terminalFocus`,151 when: `${"!".repeat(300)}terminalFocus`,
@@ -181,23 +174,23 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 22 unmodified lines
181 it.effect("bootstraps default keybindings when config file is missing", () =>174 it.effect("bootstraps default keybindings when config file is missing", () =>
182 Effect.gen(function* () {175 Effect.gen(function* () {
183 const fs = yield* FileSystem.FileSystem;176 const fs = yield* FileSystem.FileSystem;
184 const { keybindingsConfigPath } = yield* ServerConfig;177 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
185 assert.isFalse(yield* fs.exists(keybindingsConfigPath));178 assert.isFalse(yield* fs.exists(keybindingsConfigPath));
186 179
187 yield* Effect.gen(function* () {180 yield* Effect.gen(function* () {
188 const keybindings = yield* Keybindings;181 const keybindings = yield* Keybindings.Keybindings;
189 yield* keybindings.syncDefaultKeybindingsOnStartup;182 yield* keybindings.syncDefaultKeybindingsOnStartup;
190 });183 });
191 184
192 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);185 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
193 assert.deepEqual(persisted, DEFAULT_KEYBINDINGS);186 assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS);
194 }).pipe(Effect.provide(makeKeybindingsLayer())),187 }).pipe(Effect.provide(makeKeybindingsLayer())),
195 );188 );
196 189
197 it.effect("ships configurable thread navigation defaults", () =>190 it.effect("ships configurable thread navigation defaults", () =>
198 Effect.sync(() => {191 Effect.sync(() => {
199 const defaultsByCommand = new Map(192 const defaultsByCommand = new Map(
200 DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const),193 Keybindings.DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const),
201 );194 );
202 195
203 assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");196 assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");
@@ -215,17 +208,17 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 11 unmodified lines
215 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>208 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>
216 Effect.gen(function* () {209 Effect.gen(function* () {
217 const fs = yield* FileSystem.FileSystem;210 const fs = yield* FileSystem.FileSystem;
218 const { keybindingsConfigPath } = yield* ServerConfig;211 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
219 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");212 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
220 213
221 const configState = yield* Effect.gen(function* () {214 const configState = yield* Effect.gen(function* () {
222 const keybindings = yield* Keybindings;215 const keybindings = yield* Keybindings.Keybindings;
223 return yield* keybindings.loadConfigState;216 return yield* keybindings.loadConfigState;
224 });217 });
225 218
226 assert.deepEqual(219 assert.deepEqual(
227 configState.keybindings,220 configState.keybindings,
228 compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS),221 Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS),
229 );222 );
230 assert.deepEqual(configState.issues, [223 assert.deepEqual(configState.issues, [
231 {224 {
@@ -240,7 +233,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 8 unmodified lines
240 it.effect("ignores invalid entries in runtime and reports them as issues", () =>233 it.effect("ignores invalid entries in runtime and reports them as issues", () =>
241 Effect.gen(function* () {234 Effect.gen(function* () {
242 const fs = yield* FileSystem.FileSystem;235 const fs = yield* FileSystem.FileSystem;
243 const { keybindingsConfigPath } = yield* ServerConfig;236 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
244 yield* fs.writeFileString(237 yield* fs.writeFileString(
245 keybindingsConfigPath,238 keybindingsConfigPath,
246 // @effect-diagnostics-next-line preferSchemaOverJson:off239 // @effect-diagnostics-next-line preferSchemaOverJson:off
@@ -252,7 +245,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 5 unmodified lines
252 );245 );
253 246
254 const configState = yield* Effect.gen(function* () {247 const configState = yield* Effect.gen(function* () {
255 const keybindings = yield* Keybindings;248 const keybindings = yield* Keybindings.Keybindings;
256 return yield* keybindings.loadConfigState;249 return yield* keybindings.loadConfigState;
257 });250 });
258 251
@@ -279,14 +272,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 20 unmodified lines
279 "upserts missing default keybindings on startup without overriding existing command rules",272 "upserts missing default keybindings on startup without overriding existing command rules",
280 () =>273 () =>
281 Effect.gen(function* () {274 Effect.gen(function* () {
282 const { keybindingsConfigPath } = yield* ServerConfig;275 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
283 yield* writeKeybindingsConfig(keybindingsConfigPath, [276 yield* writeKeybindingsConfig(keybindingsConfigPath, [
284 { key: "mod+shift+t", command: "terminal.toggle" },277 { key: "mod+shift+t", command: "terminal.toggle" },
285 { key: "mod+shift+r", command: "script.run-tests.run" },278 { key: "mod+shift+r", command: "script.run-tests.run" },
286 ]);279 ]);
287 280
288 yield* Effect.gen(function* () {281 yield* Effect.gen(function* () {
289 const keybindings = yield* Keybindings;282 const keybindings = yield* Keybindings.Keybindings;
290 yield* keybindings.syncDefaultKeybindingsOnStartup;283 yield* keybindings.syncDefaultKeybindingsOnStartup;
291 });284 });
292 285
@@ -300,7 +293,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 7 unmodified lines
300 persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"),293 persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"),
301 );294 );
302 295
303 for (const defaultRule of DEFAULT_KEYBINDINGS) {296 for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) {
304 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);297 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);
305 }298 }
306 assert.isTrue(byCommand.has("script.run-tests.run"));299 assert.isTrue(byCommand.has("script.run-tests.run"));
@@ -314,13 +307,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 7 unmodified lines
314 });307 });
315 308
316 return Effect.gen(function* () {309 return Effect.gen(function* () {
317 const { keybindingsConfigPath } = yield* ServerConfig;310 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
318 yield* writeKeybindingsConfig(keybindingsConfigPath, [311 yield* writeKeybindingsConfig(keybindingsConfigPath, [
319 { key: "mod+j", command: "script.custom-action.run" },312 { key: "mod+j", command: "script.custom-action.run" },
320 ]);313 ]);
321 314
322 yield* Effect.gen(function* () {315 yield* Effect.gen(function* () {
323 const keybindings = yield* Keybindings;316 const keybindings = yield* Keybindings.Keybindings;
324 yield* keybindings.syncDefaultKeybindingsOnStartup;317 yield* keybindings.syncDefaultKeybindingsOnStartup;
325 });318 });
326 319
@@ -345,13 +338,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 18 unmodified lines
345 338
346 it.effect("upserts custom keybindings to configured path", () =>339 it.effect("upserts custom keybindings to configured path", () =>
347 Effect.gen(function* () {340 Effect.gen(function* () {
348 const { keybindingsConfigPath } = yield* ServerConfig;341 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
349 yield* writeKeybindingsConfig(keybindingsConfigPath, [342 yield* writeKeybindingsConfig(keybindingsConfigPath, [
350 { key: "mod+j", command: "terminal.toggle" },343 { key: "mod+j", command: "terminal.toggle" },
351 ]);344 ]);
352 345
353 const resolved = yield* Effect.gen(function* () {346 const resolved = yield* Effect.gen(function* () {
354 const keybindings = yield* Keybindings;347 const keybindings = yield* Keybindings.Keybindings;
355 return yield* keybindings.upsertKeybindingRule({348 return yield* keybindings.upsertKeybindingRule({
356 key: "mod+shift+r",349 key: "mod+shift+r",
357 command: "script.run-tests.run",350 command: "script.run-tests.run",
@@ -371,12 +364,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 13 unmodified lines
371 364
372 it.effect("appends additional custom keybindings for the same command", () =>365 it.effect("appends additional custom keybindings for the same command", () =>
373 Effect.gen(function* () {366 Effect.gen(function* () {
374 const { keybindingsConfigPath } = yield* ServerConfig;367 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
375 yield* writeKeybindingsConfig(keybindingsConfigPath, [368 yield* writeKeybindingsConfig(keybindingsConfigPath, [
376 { key: "mod+r", command: "script.run-tests.run" },369 { key: "mod+r", command: "script.run-tests.run" },
377 ]);370 ]);
378 yield* Effect.gen(function* () {371 yield* Effect.gen(function* () {
379 const keybindings = yield* Keybindings;372 const keybindings = yield* Keybindings.Keybindings;
380 return yield* keybindings.upsertKeybindingRule({373 return yield* keybindings.upsertKeybindingRule({
381 key: "mod+shift+r",374 key: "mod+shift+r",
382 command: "script.run-tests.run",375 command: "script.run-tests.run",
@@ -394,13 +387,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 11 unmodified lines
394 387
395 it.effect("replaces only the targeted custom keybinding", () =>388 it.effect("replaces only the targeted custom keybinding", () =>
396 Effect.gen(function* () {389 Effect.gen(function* () {
397 const { keybindingsConfigPath } = yield* ServerConfig;390 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
398 yield* writeKeybindingsConfig(keybindingsConfigPath, [391 yield* writeKeybindingsConfig(keybindingsConfigPath, [
399 { key: "mod+r", command: "script.run-tests.run" },392 { key: "mod+r", command: "script.run-tests.run" },
400 { key: "mod+shift+r", command: "script.run-tests.run" },393 { key: "mod+shift+r", command: "script.run-tests.run" },
401 ]);394 ]);
402 yield* Effect.gen(function* () {395 yield* Effect.gen(function* () {
403 const keybindings = yield* Keybindings;396 const keybindings = yield* Keybindings.Keybindings;
404 return yield* keybindings.upsertKeybindingRule({397 return yield* keybindings.upsertKeybindingRule({
405 key: "mod+alt+r",398 key: "mod+alt+r",
406 command: "script.run-tests.run",399 command: "script.run-tests.run",
@@ -419,13 +412,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 12 unmodified lines
419 412
420 it.effect("removes only the targeted custom keybinding", () =>413 it.effect("removes only the targeted custom keybinding", () =>
421 Effect.gen(function* () {414 Effect.gen(function* () {
422 const { keybindingsConfigPath } = yield* ServerConfig;415 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
423 yield* writeKeybindingsConfig(keybindingsConfigPath, [416 yield* writeKeybindingsConfig(keybindingsConfigPath, [
424 { key: "mod+r", command: "script.run-tests.run" },417 { key: "mod+r", command: "script.run-tests.run" },
425 { key: "mod+shift+r", command: "script.run-tests.run" },418 { key: "mod+shift+r", command: "script.run-tests.run" },
426 ]);419 ]);
427 yield* Effect.gen(function* () {420 yield* Effect.gen(function* () {
428 const keybindings = yield* Keybindings;421 const keybindings = yield* Keybindings.Keybindings;
429 return yield* keybindings.removeKeybindingRule({422 return yield* keybindings.removeKeybindingRule({
430 key: "mod+r",423 key: "mod+r",
431 command: "script.run-tests.run",424 command: "script.run-tests.run",
@@ -441,11 +434,11 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 9 unmodified lines
441 it.effect("refuses to overwrite malformed keybindings config", () =>434 it.effect("refuses to overwrite malformed keybindings config", () =>
442 Effect.gen(function* () {435 Effect.gen(function* () {
443 const fs = yield* FileSystem.FileSystem;436 const fs = yield* FileSystem.FileSystem;
444 const { keybindingsConfigPath } = yield* ServerConfig;437 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
445 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");438 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
446 439
447 const result = yield* Effect.gen(function* () {440 const result = yield* Effect.gen(function* () {
448 const keybindings = yield* Keybindings;441 const keybindings = yield* Keybindings.Keybindings;
449 return yield* keybindings.upsertKeybindingRule({442 return yield* keybindings.upsertKeybindingRule({
450 key: "mod+shift+r",443 key: "mod+shift+r",
451 command: "script.run-tests.run",444 command: "script.run-tests.run",
@@ -461,14 +454,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 9 unmodified lines
461 it.effect("reports non-array config parse errors without duplicate prefix", () =>454 it.effect("reports non-array config parse errors without duplicate prefix", () =>
462 Effect.gen(function* () {455 Effect.gen(function* () {
463 const fs = yield* FileSystem.FileSystem;456 const fs = yield* FileSystem.FileSystem;
464 const { keybindingsConfigPath } = yield* ServerConfig;457 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
465 yield* fs.writeFileString(458 yield* fs.writeFileString(
466 keybindingsConfigPath,459 keybindingsConfigPath,
467 '{"key":"mod+j","command":"terminal.toggle"}',460 '{"key":"mod+j","command":"terminal.toggle"}',
468 );461 );
469 462
470 const firstResult = yield* Effect.gen(function* () {463 const firstResult = yield* Effect.gen(function* () {
471 const keybindings = yield* Keybindings;464 const keybindings = yield* Keybindings.Keybindings;
472 return yield* keybindings.upsertKeybindingRule({465 return yield* keybindings.upsertKeybindingRule({
473 key: "mod+shift+r",466 key: "mod+shift+r",
474 command: "script.run-tests.run",467 command: "script.run-tests.run",
@@ -477,7 +470,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 2 unmodified lines
477 assertFailure(firstResult, "expected JSON array");470 assertFailure(firstResult, "expected JSON array");
478 471
479 const secondResult = yield* Effect.gen(function* () {472 const secondResult = yield* Effect.gen(function* () {
480 const keybindings = yield* Keybindings;473 const keybindings = yield* Keybindings.Keybindings;
481 return yield* keybindings.upsertKeybindingRule({474 return yield* keybindings.upsertKeybindingRule({
482 key: "mod+shift+r",475 key: "mod+shift+r",
483 command: "script.run-tests.run",476 command: "script.run-tests.run",
@@ -490,7 +483,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 6 unmodified lines
490 it.effect("fails when config directory is not writable", () =>483 it.effect("fails when config directory is not writable", () =>
491 Effect.gen(function* () {484 Effect.gen(function* () {
492 const fs = yield* FileSystem.FileSystem;485 const fs = yield* FileSystem.FileSystem;
493 const { keybindingsConfigPath } = yield* ServerConfig;486 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
494 const { dirname } = yield* Path.Path;487 const { dirname } = yield* Path.Path;
495 yield* writeKeybindingsConfig(keybindingsConfigPath, [488 yield* writeKeybindingsConfig(keybindingsConfigPath, [
496 { key: "mod+j", command: "terminal.toggle" },489 { key: "mod+j", command: "terminal.toggle" },
@@ -498,7 +491,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 1 unmodified line
498 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);491 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);
499 492
500 const result = yield* Effect.gen(function* () {493 const result = yield* Effect.gen(function* () {
501 const keybindings = yield* Keybindings;494 const keybindings = yield* Keybindings.Keybindings;
502 return yield* keybindings.upsertKeybindingRule({495 return yield* keybindings.upsertKeybindingRule({
503 key: "mod+shift+r",496 key: "mod+shift+r",
504 command: "script.run-tests.run",497 command: "script.run-tests.run",
@@ -516,13 +509,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 11 unmodified lines
516 509
517 it.effect("caches loaded resolved config across repeated reads", () =>510 it.effect("caches loaded resolved config across repeated reads", () =>
518 Effect.gen(function* () {511 Effect.gen(function* () {
519 const { keybindingsConfigPath } = yield* ServerConfig;512 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
520 yield* writeKeybindingsConfig(keybindingsConfigPath, [513 yield* writeKeybindingsConfig(keybindingsConfigPath, [
521 { key: "mod+j", command: "terminal.toggle" },514 { key: "mod+j", command: "terminal.toggle" },
522 ]);515 ]);
523 516
524 const [first, second] = yield* Effect.gen(function* () {517 const [first, second] = yield* Effect.gen(function* () {
525 const keybindings = yield* Keybindings;518 const keybindings = yield* Keybindings.Keybindings;
526 const firstLoad = (yield* keybindings.loadConfigState).keybindings;519 const firstLoad = (yield* keybindings.loadConfigState).keybindings;
527 const secondLoad = (yield* keybindings.loadConfigState).keybindings;520 const secondLoad = (yield* keybindings.loadConfigState).keybindings;
528 return [firstLoad, secondLoad] as const;521 return [firstLoad, secondLoad] as const;
@@ -535,13 +528,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 6 unmodified lines
535 528
536 it.effect("updates cached resolved config after upsert", () =>529 it.effect("updates cached resolved config after upsert", () =>
537 Effect.gen(function* () {530 Effect.gen(function* () {
538 const { keybindingsConfigPath } = yield* ServerConfig;531 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
539 yield* writeKeybindingsConfig(keybindingsConfigPath, [532 yield* writeKeybindingsConfig(keybindingsConfigPath, [
540 { key: "mod+j", command: "terminal.toggle" },533 { key: "mod+j", command: "terminal.toggle" },
541 ]);534 ]);
542 535
543 const loadedAfterUpsert = yield* Effect.gen(function* () {536 const loadedAfterUpsert = yield* Effect.gen(function* () {
544 const keybindings = yield* Keybindings;537 const keybindings = yield* Keybindings.Keybindings;
545 yield* keybindings.loadConfigState;538 yield* keybindings.loadConfigState;
546 yield* keybindings.upsertKeybindingRule({539 yield* keybindings.upsertKeybindingRule({
547 key: "mod+shift+r",540 key: "mod+shift+r",
@@ -557,7 +550,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 9 unmodified lines
557 550
558 it.effect("serializes concurrent upserts to avoid lost updates", () =>551 it.effect("serializes concurrent upserts to avoid lost updates", () =>
559 Effect.gen(function* () {552 Effect.gen(function* () {
560 const { keybindingsConfigPath } = yield* ServerConfig;553 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
561 yield* writeKeybindingsConfig(keybindingsConfigPath, []);554 yield* writeKeybindingsConfig(keybindingsConfigPath, []);
562 555
563 const commands = Array.from(556 const commands = Array.from(
@@ -565,7 +558,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { · 1 unmodified line
565 (_, index): KeybindingCommand => `script.concurrent-${index}.run`,558 (_, index): KeybindingCommand => `script.concurrent-${index}.run`,
566 );559 );
567 yield* Effect.gen(function* () {560 yield* Effect.gen(function* () {
568 const keybindings = yield* Keybindings;561 const keybindings = yield* Keybindings.Keybindings;
569 yield* Effect.all(562 yield* Effect.all(
570 commands.map((command, index) =>563 commands.map((command, index) =>
571 keybindings.upsertKeybindingRule({564 keybindings.upsertKeybindingRule({
apps/server/src/keybindings.ts+63−67
@@ -41,7 +41,7 @@ import * as Context from "effect/Context"; · 40 unmodified lines
41import * as Scope from "effect/Scope";41import * as Scope from "effect/Scope";
42import * as Stream from "effect/Stream";42import * as Stream from "effect/Stream";
43import * as Semaphore from "effect/Semaphore";43import * as Semaphore from "effect/Semaphore";
44import { ServerConfig } from "./config.ts";44import * as ServerConfig from "./config.ts";
45import { writeFileStringAtomically } from "./atomicWrite.ts";45import { writeFileStringAtomically } from "./atomicWrite.ts";
46import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";46import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
47import {47import {
@@ -225,74 +225,70 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve · 177 unmodified lines
225 return merged.slice(-MAX_KEYBINDINGS_COUNT);225 return merged.slice(-MAX_KEYBINDINGS_COUNT);
226}226}
227 227
228/**
229 * KeybindingsShape - Service API for keybinding configuration operations.
230 */
231export interface KeybindingsShape {
232 /**
233 * Start the keybindings runtime and attach file watching.
234 *
235 * Safe to call multiple times. The first successful call establishes the
236 * runtime; later calls await the same startup.
237 */
238 readonly start: Effect.Effect<void, KeybindingsConfigError>;
239
240 /**
241 * Await keybindings runtime readiness.
242 *
243 * Readiness means the config directory exists, the watcher is attached, the
244 * startup sync has completed, and the current snapshot has been loaded.
245 */
246 readonly ready: Effect.Effect<void, KeybindingsConfigError>;
247
248 /**
249 * Ensure the on-disk keybindings file exists and includes all default
250 * commands so newly-added defaults are backfilled on startup.
251 */
252 readonly syncDefaultKeybindingsOnStartup: Effect.Effect<void, KeybindingsConfigError>;
253
254 /**
255 * Load runtime keybindings state along with non-fatal configuration issues.
256 */
257 readonly loadConfigState: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
258
259 /**
260 * Read the latest keybindings snapshot from cache/disk.
261 */
262 readonly getSnapshot: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
263
264 /**
265 * Stream of keybindings config change events.
266 */
267 readonly streamChanges: Stream.Stream<KeybindingsChangeEvent>;
268
269 /**
270 * Upsert a keybinding rule and persist the resulting configuration.
271 *
272 * Writes config atomically and enforces the max rule count by truncating
273 * oldest entries when needed.
274 */
275 readonly upsertKeybindingRule: (
276 input: ServerUpsertKeybindingInput,
277 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
278
279 /**
280 * Remove a single persisted keybinding rule by exact key/command/when match.
281 */
282 readonly removeKeybindingRule: (
283 input: ServerRemoveKeybindingInput,
284 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
285}
286
287/**228/**
288 * Keybindings - Service tag for keybinding configuration operations.229 * Keybindings - Service tag for keybinding configuration operations.
289 */230 */
290export class Keybindings extends Context.Service<Keybindings, KeybindingsShape>()(231export class Keybindings extends Context.Service<
291 "t3/keybindings",232 Keybindings,
292) {}233 {
234 /**
235 * Start the keybindings runtime and attach file watching.
236 *
237 * Safe to call multiple times. The first successful call establishes the
238 * runtime; later calls await the same startup.
239 */
240 readonly start: Effect.Effect<void, KeybindingsConfigError>;
241
242 /**
243 * Await keybindings runtime readiness.
244 *
245 * Readiness means the config directory exists, the watcher is attached, the
246 * startup sync has completed, and the current snapshot has been loaded.
247 */
248 readonly ready: Effect.Effect<void, KeybindingsConfigError>;
249
250 /**
251 * Ensure the on-disk keybindings file exists and includes all default
252 * commands so newly-added defaults are backfilled on startup.
253 */
254 readonly syncDefaultKeybindingsOnStartup: Effect.Effect<void, KeybindingsConfigError>;
255
256 /**
257 * Load runtime keybindings state along with non-fatal configuration issues.
258 */
259 readonly loadConfigState: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
260
261 /**
262 * Read the latest keybindings snapshot from cache/disk.
263 */
264 readonly getSnapshot: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
265
266 /**
267 * Stream of keybindings config change events.
268 */
269 readonly streamChanges: Stream.Stream<KeybindingsChangeEvent>;
270
271 /**
272 * Upsert a keybinding rule and persist the resulting configuration.
273 *
274 * Writes config atomically and enforces the max rule count by truncating
275 * oldest entries when needed.
276 */
277 readonly upsertKeybindingRule: (
278 input: ServerUpsertKeybindingInput,
279 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
280
281 /**
282 * Remove a single persisted keybinding rule by exact key/command/when match.
283 */
284 readonly removeKeybindingRule: (
285 input: ServerRemoveKeybindingInput,
286 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
287 }
288>()("t3/keybindings") {}
293 289
294const makeKeybindings = Effect.gen(function* () {290const make = Effect.gen(function* () {
295 const { keybindingsConfigPath } = yield* ServerConfig;291 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
296 const fs = yield* FileSystem.FileSystem;292 const fs = yield* FileSystem.FileSystem;
297 const path = yield* Path.Path;293 const path = yield* Path.Path;
298 const upsertSemaphore = yield* Semaphore.make(1);294 const upsertSemaphore = yield* Semaphore.make(1);
@@ -700,7 +696,7 @@ const makeKeybindings = Effect.gen(function* () { · 401 unmodified lines
700 return nextResolved;696 return nextResolved;
701 }),697 }),
702 ),698 ),
703 } satisfies KeybindingsShape;699 } satisfies Keybindings["Service"];
704});700});
705 701
706export const KeybindingsLive = Layer.effect(Keybindings, makeKeybindings);702export const layer = Layer.effect(Keybindings, make);
apps/server/src/mcp/McpHttpServer.test.ts+9−1
@@ -107,7 +107,15 @@ it.effect("registers annotated tools and preserves authenticated request context · 106 unmodified lines
107 Effect.gen(function* () {107 Effect.gen(function* () {
108 const server = yield* McpServer.McpServer;108 const server = yield* McpServer.McpServer;
109 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;109 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
110 const requests = yield* broker.connect("mcp-test-client");110 const requests = yield* broker.connect({
111 clientId: "mcp-test-client",
112 environmentId,
113 threadId,
114 tabId,
115 visible: true,
116 supportsAutomation: true,
117 focusedAt: "2026-06-11T00:00:00.000Z",
118 });
111 yield* Stream.runForEach(requests, (request) =>119 yield* Stream.runForEach(requests, (request) =>
112 broker.respond({120 broker.respond({
113 requestId: request.requestId,121 requestId: request.requestId,
apps/server/src/mcp/PreviewAutomationBroker.test.ts+122−20
@@ -2,10 +2,13 @@ import { expect, it } from "@effect/vitest"; · 1 unmodified line
2import {2import {
3 EnvironmentId,3 EnvironmentId,
4 PreviewAutomationNoFocusedOwnerError,4 PreviewAutomationNoFocusedOwnerError,
5 PreviewAutomationUnavailableError,
5 ProviderInstanceId,6 ProviderInstanceId,
6 ThreadId,7 ThreadId,
8 type PreviewAutomationOwner,
7} from "@t3tools/contracts";9} from "@t3tools/contracts";
8import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import * as Fiber from "effect/Fiber";
9import * as Stream from "effect/Stream";12import * as Stream from "effect/Stream";
10 13
11import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";14import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
@@ -20,11 +23,22 @@ const scope = { · 8 unmodified lines
20 expiresAt: 2,23 expiresAt: 2,
21};24};
22 25
23it.effect("routes a request to the focused owner and correlates its response", () =>26const makeOwner = (overrides: Partial<PreviewAutomationOwner> = {}): PreviewAutomationOwner => ({
27 clientId: "client-1",
28 environmentId: scope.environmentId,
29 threadId: scope.threadId,
30 tabId: null,
31 visible: false,
32 supportsAutomation: true,
33 focusedAt: "2026-06-11T00:00:00.000Z",
34 ...overrides,
35});
36
37it.effect("atomically registers a connected owner and correlates its response", () =>
24 Effect.scoped(38 Effect.scoped(
25 Effect.gen(function* () {39 Effect.gen(function* () {
26 const broker = yield* PreviewAutomationBroker.__testing.make;40 const broker = yield* PreviewAutomationBroker.__testing.make;
27 const requests = yield* broker.connect("client-1");41 const requests = yield* broker.connect(makeOwner());
28 yield* Stream.runForEach(requests, (request) =>42 yield* Stream.runForEach(requests, (request) =>
29 broker.respond({43 broker.respond({
30 requestId: request.requestId,44 requestId: request.requestId,
@@ -33,15 +47,6 @@ it.effect("routes a request to the focused owner and correlates its response", ( · 2 unmodified lines
33 }),47 }),
34 ).pipe(Effect.forkScoped);48 ).pipe(Effect.forkScoped);
35 yield* Effect.yieldNow;49 yield* Effect.yieldNow;
36 yield* broker.reportOwner({
37 clientId: "client-1",
38 environmentId: scope.environmentId,
39 threadId: scope.threadId,
40 tabId: null,
41 visible: false,
42 supportsAutomation: true,
43 focusedAt: "2026-06-11T00:00:00.000Z",
44 });
45 50
46 const result = yield* broker.invoke<{ available: boolean }>({51 const result = yield* broker.invoke<{ available: boolean }>({
47 scope,52 scope,
@@ -68,22 +73,119 @@ it.effect("routes interactive commands to a hidden durable browser host", () => · 20 unmodified lines
68 Effect.scoped(73 Effect.scoped(
69 Effect.gen(function* () {74 Effect.gen(function* () {
70 const broker = yield* PreviewAutomationBroker.__testing.make;75 const broker = yield* PreviewAutomationBroker.__testing.make;
71 const requests = yield* broker.connect("client-hidden");76 const requests = yield* broker.connect(
77 makeOwner({ clientId: "client-hidden", tabId: "tab-hidden" }),
78 );
79 yield* Stream.runForEach(requests, (request) =>
80 broker.respond({ requestId: request.requestId, ok: true }),
81 ).pipe(Effect.forkScoped);
82 yield* Effect.yieldNow;
83
84 yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });
85 }),
86 ),
87);
88
89it.effect("lets the browser host resolve an active tab that has not been reported yet", () =>
90 Effect.scoped(
91 Effect.gen(function* () {
92 const broker = yield* PreviewAutomationBroker.__testing.make;
93 const requests = yield* broker.connect(makeOwner({ tabId: null }));
94 let routedTabId: string | undefined;
95 yield* Stream.runForEach(requests, (request) => {
96 routedTabId = request.tabId;
97 return broker.respond({ requestId: request.requestId, ok: true });
98 }).pipe(Effect.forkScoped);
99 yield* Effect.yieldNow;
100
101 yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });
102
103 expect(routedTabId).toBeUndefined();
104 }),
105 ),
106);
107
108it.effect("preserves current owner metadata when its request stream reconnects", () =>
109 Effect.scoped(
110 Effect.gen(function* () {
111 const broker = yield* PreviewAutomationBroker.__testing.make;
112 const firstRequests = yield* broker.connect(makeOwner());
113 yield* Stream.runDrain(firstRequests).pipe(Effect.forkScoped);
114 yield* broker.reportOwner(makeOwner({ tabId: "tab-current", visible: true }));
115
116 const reconnectedRequests = yield* broker.connect(makeOwner());
117 let routedTabId: string | undefined;
118 yield* Stream.runForEach(reconnectedRequests, (request) => {
119 routedTabId = request.tabId;
120 return broker.respond({ requestId: request.requestId, ok: true });
121 }).pipe(Effect.forkScoped);
122 yield* Effect.yieldNow;
123
124 yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });
125
126 expect(routedTabId).toBe("tab-current");
127 }),
128 ),
129);
130
131it.effect("ignores stale owner cleanup after the client moves to another thread", () =>
132 Effect.scoped(
133 Effect.gen(function* () {
134 const broker = yield* PreviewAutomationBroker.__testing.make;
135 const requests = yield* broker.connect(makeOwner());
72 yield* Stream.runForEach(requests, (request) =>136 yield* Stream.runForEach(requests, (request) =>
73 broker.respond({ requestId: request.requestId, ok: true }),137 broker.respond({ requestId: request.requestId, ok: true }),
74 ).pipe(Effect.forkScoped);138 ).pipe(Effect.forkScoped);
75 yield* Effect.yieldNow;139 yield* Effect.yieldNow;
76 yield* broker.reportOwner({140
77 clientId: "client-hidden",141 yield* broker.clearOwner({
142 clientId: "client-1",
78 environmentId: scope.environmentId,143 environmentId: scope.environmentId,
79 threadId: scope.threadId,144 threadId: ThreadId.make("thread-stale"),
80 tabId: "tab-hidden",
81 visible: false,
82 supportsAutomation: true,
83 focusedAt: "2026-06-11T00:00:00.000Z",
84 });145 });
85 146
86 yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });147 yield* broker.invoke<void>({ scope, operation: "status", input: {} });
148 }),
149 ),
150);
151
152it.effect("fails requests assigned to a browser stream when that stream reconnects", () =>
153 Effect.scoped(
154 Effect.gen(function* () {
155 const broker = yield* PreviewAutomationBroker.__testing.make;
156 const _requests = yield* broker.connect(makeOwner());
157 const pending = yield* broker
158 .invoke<void>({ scope, operation: "status", input: {} })
159 .pipe(Effect.flip, Effect.forkScoped);
160 yield* Effect.yieldNow;
161
162 const _replacementRequests = yield* broker.connect(makeOwner());
163
164 const error = yield* Fiber.join(pending);
165 expect(error).toBeInstanceOf(PreviewAutomationUnavailableError);
166 }),
167 ),
168);
169
170it.effect("falls back to an older connected owner when a newer report is not connected", () =>
171 Effect.scoped(
172 Effect.gen(function* () {
173 const broker = yield* PreviewAutomationBroker.__testing.make;
174 const requests = yield* broker.connect(makeOwner({ clientId: "client-connected" }));
175 yield* Stream.runForEach(requests, (request) =>
176 broker.respond({ requestId: request.requestId, ok: true, result: "connected" }),
177 ).pipe(Effect.forkScoped);
178 yield* Effect.yieldNow;
179 yield* broker.reportOwner(
180 makeOwner({
181 clientId: "client-report-only",
182 focusedAt: "2026-06-11T00:00:01.000Z",
183 }),
184 );
185
186 const result = yield* broker.invoke<string>({ scope, operation: "status", input: {} });
187
188 expect(result).toBe("connected");
87 }),189 }),
88 ),190 ),
89);191);
apps/server/src/mcp/PreviewAutomationBroker.ts+40−25
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
11 type PreviewAutomationError,11 type PreviewAutomationError,
12 type PreviewAutomationOperation,12 type PreviewAutomationOperation,
13 type PreviewAutomationOwner,13 type PreviewAutomationOwner,
14 type PreviewAutomationOwnerIdentity,
14 type PreviewAutomationRequest,15 type PreviewAutomationRequest,
15 type PreviewAutomationResponse,16 type PreviewAutomationResponse,
16 type PreviewTabId,17 type PreviewTabId,
@@ -35,11 +36,13 @@ export interface PreviewAutomationInvokeInput { · 18 unmodified lines
35}36}
36 37
37export interface PreviewAutomationBrokerShape {38export interface PreviewAutomationBrokerShape {
38 readonly connect: (clientId: string) => Effect.Effect<Stream.Stream<PreviewAutomationRequest>>;39 readonly connect: (
40 owner: PreviewAutomationOwner,
41 ) => Effect.Effect<Stream.Stream<PreviewAutomationRequest>>;
39 readonly reportOwner: (42 readonly reportOwner: (
40 owner: PreviewAutomationOwner,43 owner: PreviewAutomationOwner,
41 ) => Effect.Effect<void, PreviewAutomationError>;44 ) => Effect.Effect<void, PreviewAutomationError>;
42 readonly clearOwner: (clientId: string) => Effect.Effect<void>;45 readonly clearOwner: (owner: PreviewAutomationOwnerIdentity) => Effect.Effect<void>;
43 readonly respond: (46 readonly respond: (
44 response: PreviewAutomationResponse,47 response: PreviewAutomationResponse,
45 ) => Effect.Effect<void, PreviewAutomationError>;48 ) => Effect.Effect<void, PreviewAutomationError>;
@@ -63,7 +66,7 @@ interface ClientConnection { · 17 unmodified lines
63}66}
64 67
65interface PendingRequest {68interface PendingRequest {
66 readonly clientId: string;69 readonly queue: ClientConnection["queue"];
67 readonly deferred: Deferred.Deferred<unknown, PreviewAutomationError>;70 readonly deferred: Deferred.Deferred<unknown, PreviewAutomationError>;
68}71}
69 72
@@ -133,17 +136,16 @@ const make = Effect.gen(function* PreviewAutomationBrokerMake() { · 63 unmodified lines
133 queue: ClientConnection["queue"],136 queue: ClientConnection["queue"],
134 ) {137 ) {
135 const toFail = yield* SynchronizedRef.modify(state, (current) => {138 const toFail = yield* SynchronizedRef.modify(state, (current) => {
136 if (current.clients.get(clientId)?.queue !== queue) {
137 return [[] as ReadonlyArray<PendingRequest>, current] as const;
138 }
139 const clients = new Map(current.clients);139 const clients = new Map(current.clients);
140 const owners = new Map(current.owners);140 const owners = new Map(current.owners);
141 const pending = new Map(current.pending);141 const pending = new Map(current.pending);
142 const disconnected: PendingRequest[] = [];142 const disconnected: PendingRequest[] = [];
143 clients.delete(clientId);143 if (current.clients.get(clientId)?.queue === queue) {
144 owners.delete(clientId);144 clients.delete(clientId);
145 owners.delete(clientId);
146 }
145 for (const [requestId, entry] of pending) {147 for (const [requestId, entry] of pending) {
146 if (entry.clientId === clientId) {148 if (entry.queue === queue) {
147 pending.delete(requestId);149 pending.delete(requestId);
148 disconnected.push(entry);150 disconnected.push(entry);
149 }151 }
@@ -166,12 +168,22 @@ const make = Effect.gen(function* PreviewAutomationBrokerMake() { · 16 unmodified lines
166 168
167 const connect: PreviewAutomationBrokerShape["connect"] = Effect.fn(169 const connect: PreviewAutomationBrokerShape["connect"] = Effect.fn(
168 "PreviewAutomationBroker.connect",170 "PreviewAutomationBroker.connect",
169 )(function* (clientId) {171 )(function* (owner) {
172 const clientId = owner.clientId;
170 const queue = yield* Queue.unbounded<import("@t3tools/contracts").PreviewAutomationRequest>();173 const queue = yield* Queue.unbounded<import("@t3tools/contracts").PreviewAutomationRequest>();
171 const previous = yield* SynchronizedRef.modify(state, (current) => {174 const previous = yield* SynchronizedRef.modify(state, (current) => {
172 const clients = new Map(current.clients);175 const clients = new Map(current.clients);
176 const owners = new Map(current.owners);
177 const existingOwner = current.owners.get(clientId);
173 clients.set(clientId, { clientId, queue });178 clients.set(clientId, { clientId, queue });
174 return [current.clients.get(clientId), { ...current, clients }] as const;179 owners.set(
180 clientId,
181 existingOwner?.environmentId === owner.environmentId &&
182 existingOwner.threadId === owner.threadId
183 ? { ...existingOwner, supportsAutomation: owner.supportsAutomation }
184 : owner,
185 );
186 return [current.clients.get(clientId), { ...current, clients, owners }] as const;
175 });187 });
176 if (previous) yield* disconnect(clientId, previous.queue);188 if (previous) yield* disconnect(clientId, previous.queue);
177 return Stream.fromQueue(queue).pipe(Stream.ensuring(disconnect(clientId, queue)));189 return Stream.fromQueue(queue).pipe(Stream.ensuring(disconnect(clientId, queue)));
@@ -189,10 +201,18 @@ const make = Effect.gen(function* PreviewAutomationBrokerMake() { · 11 unmodified lines
189 201
190 const clearOwner: PreviewAutomationBrokerShape["clearOwner"] = Effect.fn(202 const clearOwner: PreviewAutomationBrokerShape["clearOwner"] = Effect.fn(
191 "PreviewAutomationBroker.clearOwner",203 "PreviewAutomationBroker.clearOwner",
192 )(function* (clientId) {204 )(function* (owner) {
193 yield* SynchronizedRef.update(state, (current) => {205 yield* SynchronizedRef.update(state, (current) => {
206 const currentOwner = current.owners.get(owner.clientId);
207 if (
208 !currentOwner ||
209 currentOwner.environmentId !== owner.environmentId ||
210 currentOwner.threadId !== owner.threadId
211 ) {
212 return current;
213 }
194 const owners = new Map(current.owners);214 const owners = new Map(current.owners);
195 owners.delete(clientId);215 owners.delete(owner.clientId);
196 return { ...current, owners };216 return { ...current, owners };
197 });217 });
198 });218 });
@@ -234,8 +254,13 @@ const make = Effect.gen(function* PreviewAutomationBrokerMake() { · 35 unmodified lines
234 owner.supportsAutomation,254 owner.supportsAutomation,
235 )255 )
236 .sort((left, right) => right.focusedAt.localeCompare(left.focusedAt));256 .sort((left, right) => right.focusedAt.localeCompare(left.focusedAt));
237 const owner = candidates[0];257 const owner = candidates.find((candidate) => current.clients.has(candidate.clientId));
238 if (!owner) {258 if (!owner) {
259 if (candidates.length > 0) {
260 return yield* new PreviewAutomationUnavailableError({
261 message: "The browser host is not connected.",
262 });
263 }
239 return yield* new PreviewAutomationNoFocusedOwnerError({264 return yield* new PreviewAutomationNoFocusedOwnerError({
240 message: "No desktop browser host is available for this thread.",265 message: "No desktop browser host is available for this thread.",
241 });266 });
@@ -246,22 +271,12 @@ const make = Effect.gen(function* PreviewAutomationBrokerMake() { · 4 unmodified lines
246 message: "The browser host is not connected.",271 message: "The browser host is not connected.",
247 });272 });
248 }273 }
249 if (
250 input.operation !== "open" &&
251 input.operation !== "status" &&
252 !owner.tabId &&
253 !input.tabId
254 ) {
255 return yield* new PreviewAutomationTabNotFoundError({
256 message: "The browser host does not have an active tab.",
257 });
258 }
259 const timeoutMs = input.timeoutMs ?? 15_000;274 const timeoutMs = input.timeoutMs ?? 15_000;
260 const deferred = yield* Deferred.make<unknown, PreviewAutomationError>();275 const deferred = yield* Deferred.make<unknown, PreviewAutomationError>();
261 const requestId = yield* SynchronizedRef.modify(state, (next) => {276 const requestId = yield* SynchronizedRef.modify(state, (next) => {
262 const requestId = `preview-${next.requestSequence}`;277 const requestId = `preview-${next.requestSequence}`;
263 const pending = new Map(next.pending);278 const pending = new Map(next.pending);
264 pending.set(requestId, { clientId: owner.clientId, deferred });279 pending.set(requestId, { queue: connection.queue, deferred });
265 return [requestId, { ...next, pending, requestSequence: next.requestSequence + 1 }] as const;280 return [requestId, { ...next, pending, requestSequence: next.requestSequence + 1 }] as const;
266 });281 });
267 const removePending = SynchronizedRef.update(state, (next) => {282 const removePending = SynchronizedRef.update(state, (next) => {
apps/server/src/provider/Layers/ProviderRegistry.test.ts+116−76
@@ -32,8 +32,8 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; · 31 unmodified lines
32 32
33import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";33import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
34import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";34import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
35import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts";35import * as OpenCodeRuntime from "../opencodeRuntime.ts";
36import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";36import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
37import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";37import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";
38import {38import {
39 haveProvidersChanged,39 haveProvidersChanged,
@@ -42,12 +42,12 @@ import { · 2 unmodified lines
42 ProviderRegistryLive,42 ProviderRegistryLive,
43 selectProvidersByKind,43 selectProvidersByKind,
44} from "./ProviderRegistry.ts";44} from "./ProviderRegistry.ts";
45import { ServerConfig } from "../../config.ts";45import * as ServerConfig from "../../config.ts";
46import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";46import * as ServerSettingsModule from "../../serverSettings.ts";
47import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";47import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";
48import type { ProviderInstance } from "../ProviderDriver.ts";48import type { ProviderInstance } from "../ProviderDriver.ts";
49import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";49import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
50import { ProviderRegistry } from "../Services/ProviderRegistry.ts";50import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
51import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";51import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
52const decodeServerSettings = Schema.decodeSync(ServerSettings);52const decodeServerSettings = Schema.decodeSync(ServerSettings);
53const encodeServerSettings = Schema.encodeSync(ServerSettings);53const encodeServerSettings = Schema.encodeSync(ServerSettings);
@@ -294,11 +294,11 @@ function makeMutableServerSettingsService( · 240 unmodified lines
294 get streamChanges() {294 get streamChanges() {
295 return Stream.fromPubSub(changes);295 return Stream.fromPubSub(changes);
296 },296 },
297 } satisfies ServerSettingsShape;297 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
298 });298 });
299}299}
300 300
301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), TestHttpClientLive))(301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(
302 "ProviderRegistry",302 "ProviderRegistry",
303 (it) => {303 (it) => {
304 describe("checkCodexProviderStatus", () => {304 describe("checkCodexProviderStatus", () => {
@@ -636,14 +636,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 331 unmodified lines
636 adapter: {} as ProviderInstance["adapter"],636 adapter: {} as ProviderInstance["adapter"],
637 textGeneration: {} as ProviderInstance["textGeneration"],637 textGeneration: {} as ProviderInstance["textGeneration"],
638 } satisfies ProviderInstance;638 } satisfies ProviderInstance;
639 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {639 const instanceRegistryLayer = Layer.succeed(
640 getInstance: (instanceId) =>640 ProviderInstanceRegistry.ProviderInstanceRegistry,
641 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),641 {
642 listInstances: Effect.succeed([instance]),642 getInstance: (instanceId) =>
643 listUnavailable: Effect.succeed([]),643 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
644 streamChanges: Stream.empty,644 listInstances: Effect.succeed([instance]),
645 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),645 listUnavailable: Effect.succeed([]),
646 });646 streamChanges: Stream.empty,
647 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
648 },
649 );
647 const scope = yield* Scope.make();650 const scope = yield* Scope.make();
648 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));651 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
649 const runtimeServices = yield* Layer.build(652 const runtimeServices = yield* Layer.build(
@@ -658,7 +661,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 8 unmodified lines
658 ),661 ),
659 ).pipe(Scope.provide(scope));662 ).pipe(Scope.provide(scope));
660 yield* Effect.gen(function* () {663 yield* Effect.gen(function* () {
661 const registry = yield* ProviderRegistry;664 const registry = yield* ProviderRegistry.ProviderRegistry;
662 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);665 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
663 assert.strictEqual(yield* Ref.get(refreshCalls), 0);666 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
664 }).pipe(Effect.provide(runtimeServices));667 }).pipe(Effect.provide(runtimeServices));
@@ -786,16 +789,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 121 unmodified lines
786 adapter: {} as ProviderInstance["adapter"],789 adapter: {} as ProviderInstance["adapter"],
787 textGeneration: {} as ProviderInstance["textGeneration"],790 textGeneration: {} as ProviderInstance["textGeneration"],
788 } satisfies ProviderInstance;791 } satisfies ProviderInstance;
789 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {792 const instanceRegistryLayer = Layer.succeed(
790 getInstance: (instanceId) =>793 ProviderInstanceRegistry.ProviderInstanceRegistry,
791 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),794 {
792 listInstances: Effect.succeed([instance]),795 getInstance: (instanceId) =>
793 listUnavailable: Effect.succeed([]),796 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
794 streamChanges: Stream.empty,797 listInstances: Effect.succeed([instance]),
795 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>798 listUnavailable: Effect.succeed([]),
796 PubSub.subscribe(pubsub),799 streamChanges: Stream.empty,
797 ),800 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
798 });801 PubSub.subscribe(pubsub),
802 ),
803 },
804 );
799 const scope = yield* Scope.make();805 const scope = yield* Scope.make();
800 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));806 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
801 const runtimeServices = yield* Layer.build(807 const runtimeServices = yield* Layer.build(
@@ -811,8 +817,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 9 unmodified lines
811 ).pipe(Scope.provide(scope));817 ).pipe(Scope.provide(scope));
812 818
813 yield* Effect.gen(function* () {819 yield* Effect.gen(function* () {
814 const registry = yield* ProviderRegistry;820 const registry = yield* ProviderRegistry.ProviderRegistry;
815 const config = yield* ServerConfig;821 const config = yield* ServerConfig.ServerConfig;
816 const filePath = yield* resolveProviderStatusCachePath({822 const filePath = yield* resolveProviderStatusCachePath({
817 cacheDir: config.providerStatusCacheDir,823 cacheDir: config.providerStatusCacheDir,
818 instanceId: cursorInstanceId,824 instanceId: cursorInstanceId,
@@ -880,16 +886,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 61 unmodified lines
880 adapter: {} as ProviderInstance["adapter"],886 adapter: {} as ProviderInstance["adapter"],
881 textGeneration: {} as ProviderInstance["textGeneration"],887 textGeneration: {} as ProviderInstance["textGeneration"],
882 } satisfies ProviderInstance;888 } satisfies ProviderInstance;
883 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {889 const instanceRegistryLayer = Layer.succeed(
884 getInstance: (instanceId) =>890 ProviderInstanceRegistry.ProviderInstanceRegistry,
885 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),891 {
886 listInstances: Effect.succeed([instance]),892 getInstance: (instanceId) =>
887 listUnavailable: Effect.succeed([]),893 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
888 streamChanges: Stream.empty,894 listInstances: Effect.succeed([instance]),
889 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>895 listUnavailable: Effect.succeed([]),
890 PubSub.subscribe(pubsub),896 streamChanges: Stream.empty,
891 ),897 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
892 });898 PubSub.subscribe(pubsub),
899 ),
900 },
901 );
893 const scope = yield* Scope.make();902 const scope = yield* Scope.make();
894 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));903 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
895 const runtimeServices = yield* Layer.build(904 const runtimeServices = yield* Layer.build(
@@ -905,7 +914,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 9 unmodified lines
905 ).pipe(Scope.provide(scope));914 ).pipe(Scope.provide(scope));
906 915
907 yield* Effect.gen(function* () {916 yield* Effect.gen(function* () {
908 const registry = yield* ProviderRegistry;917 const registry = yield* ProviderRegistry.ProviderRegistry;
909 918
910 assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]);919 assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]);
911 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]);920 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]);
@@ -975,25 +984,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 63 unmodified lines
975 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);984 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
976 const failNextList = yield* Ref.make(false);985 const failNextList = yield* Ref.make(false);
977 const wait = () => Effect.yieldNow;986 const wait = () => Effect.yieldNow;
978 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {987 const instanceRegistryLayer = Layer.succeed(
979 getInstance: (instanceId) =>988 ProviderInstanceRegistry.ProviderInstanceRegistry,
980 Ref.get(instancesRef).pipe(989 {
981 Effect.map((instances) =>990 getInstance: (instanceId) =>
982 instances.find((instance) => instance.instanceId === instanceId),991 Ref.get(instancesRef).pipe(
992 Effect.map((instances) =>
993 instances.find((instance) => instance.instanceId === instanceId),
994 ),
983 ),995 ),
984 ),996 listInstances: Effect.gen(function* () {
985 listInstances: Effect.gen(function* () {997 const shouldFail = yield* Ref.get(failNextList);
986 const shouldFail = yield* Ref.get(failNextList);998 if (shouldFail) {
987 if (shouldFail) {999 yield* Ref.set(failNextList, false);
988 yield* Ref.set(failNextList, false);1000 return yield* Effect.die(new Error("simulated registry list failure"));
989 return yield* Effect.die(new Error("simulated registry list failure"));1001 }
990 }1002 return yield* Ref.get(instancesRef);
991 return yield* Ref.get(instancesRef);1003 }),
992 }),1004 listUnavailable: Effect.succeed([]),
993 listUnavailable: Effect.succeed([]),1005 streamChanges: Stream.fromPubSub(changes),
994 streamChanges: Stream.fromPubSub(changes),1006 subscribeChanges: PubSub.subscribe(changes),
995 subscribeChanges: PubSub.subscribe(changes),1007 },
996 });1008 );
997 const scope = yield* Scope.make();1009 const scope = yield* Scope.make();
998 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1010 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
999 const runtimeServices = yield* Layer.build(1011 const runtimeServices = yield* Layer.build(
@@ -1009,7 +1021,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 9 unmodified lines
1009 ).pipe(Scope.provide(scope));1021 ).pipe(Scope.provide(scope));
1010 1022
1011 yield* Effect.gen(function* () {1023 yield* Effect.gen(function* () {
1012 const registry = yield* ProviderRegistry;1024 const registry = yield* ProviderRegistry.ProviderRegistry;
1013 assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]);1025 assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]);
1014 1026
1015 yield* Ref.set(failNextList, true);1027 yield* Ref.set(failNextList, true);
@@ -1092,15 +1104,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 76 unmodified lines
1092 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1104 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1093 const providerRegistryLayer = ProviderRegistryLive.pipe(1105 const providerRegistryLayer = ProviderRegistryLive.pipe(
1094 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1106 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1095 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1107 Layer.provideMerge(
1108 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1109 ),
1096 Layer.provideMerge(1110 Layer.provideMerge(
1097 ServerConfig.layerTest(process.cwd(), {1111 ServerConfig.layerTest(process.cwd(), {
1098 prefix: "t3-provider-registry-",1112 prefix: "t3-provider-registry-",
1099 }),1113 }),
1100 ),1114 ),
1101 Layer.provideMerge(TestHttpClientLive),1115 Layer.provideMerge(TestHttpClientLive),
1102 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1116 Layer.provideMerge(
1103 Layer.provideMerge(OpenCodeRuntimeLive),1117 Layer.succeed(
1118 ProviderEventLoggers.ProviderEventLoggers,
1119 ProviderEventLoggers.NoOpProviderEventLoggers,
1120 ),
1121 ),
1122 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1104 // NO spawner mock — `ChildProcessSpawner` is supplied by the1123 // NO spawner mock — `ChildProcessSpawner` is supplied by the
1105 // outer `NodeServices.layer` on `it.layer(...)` and will1124 // outer `NodeServices.layer` on `it.layer(...)` and will
1106 // genuinely spawn a subprocess. The missing-binary ENOENT is1125 // genuinely spawn a subprocess. The missing-binary ENOENT is
@@ -1112,7 +1131,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 5 unmodified lines
1112 );1131 );
1113 1132
1114 yield* Effect.gen(function* () {1133 yield* Effect.gen(function* () {
1115 const registry = yield* ProviderRegistry;1134 const registry = yield* ProviderRegistry.ProviderRegistry;
1116 let providers = yield* registry.getProviders;1135 let providers = yield* registry.getProviders;
1117 for (1136 for (
1118 let attempts = 0;1137 let attempts = 0;
@@ -1177,15 +1196,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 58 unmodified lines
1177 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1196 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1178 const providerRegistryLayer = ProviderRegistryLive.pipe(1197 const providerRegistryLayer = ProviderRegistryLive.pipe(
1179 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1198 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1180 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1199 Layer.provideMerge(
1200 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1201 ),
1181 Layer.provideMerge(1202 Layer.provideMerge(
1182 ServerConfig.layerTest(process.cwd(), {1203 ServerConfig.layerTest(process.cwd(), {
1183 prefix: "t3-provider-registry-",1204 prefix: "t3-provider-registry-",
1184 }),1205 }),
1185 ),1206 ),
1186 Layer.provideMerge(TestHttpClientLive),1207 Layer.provideMerge(TestHttpClientLive),
1187 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1208 Layer.provideMerge(
1188 Layer.provideMerge(OpenCodeRuntimeLive),1209 Layer.succeed(
1210 ProviderEventLoggers.ProviderEventLoggers,
1211 ProviderEventLoggers.NoOpProviderEventLoggers,
1212 ),
1213 ),
1214 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1189 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>1215 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
1190 ChildProcessSpawner.make((command) => {1216 ChildProcessSpawner.make((command) => {
1191 spawnedCommands.push((command as { readonly command: string }).command);1217 spawnedCommands.push((command as { readonly command: string }).command);
@@ -1199,7 +1225,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 7 unmodified lines
1199 );1225 );
1200 1226
1201 yield* Effect.gen(function* () {1227 yield* Effect.gen(function* () {
1202 const registry = yield* ProviderRegistry;1228 const registry = yield* ProviderRegistry.ProviderRegistry;
1203 // Boot-time probe: the default codex instance is enabled with1229 // Boot-time probe: the default codex instance is enabled with
1204 // `firstMissing`, so the real spawner yields ENOENT and the1230 // `firstMissing`, so the real spawner yields ENOENT and the
1205 // snapshot should be `status: "error"`.1231 // snapshot should be `status: "error"`.
@@ -1291,15 +1317,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 85 unmodified lines
1291 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1317 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1292 const providerRegistryLayer = ProviderRegistryLive.pipe(1318 const providerRegistryLayer = ProviderRegistryLive.pipe(
1293 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1319 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1294 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1320 Layer.provideMerge(
1321 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1322 ),
1295 Layer.provideMerge(1323 Layer.provideMerge(
1296 ServerConfig.layerTest(process.cwd(), {1324 ServerConfig.layerTest(process.cwd(), {
1297 prefix: "t3-provider-registry-",1325 prefix: "t3-provider-registry-",
1298 }),1326 }),
1299 ),1327 ),
1300 Layer.provideMerge(TestHttpClientLive),1328 Layer.provideMerge(TestHttpClientLive),
1301 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1329 Layer.provideMerge(
1302 Layer.provideMerge(OpenCodeRuntimeLive),1330 Layer.succeed(
1331 ProviderEventLoggers.ProviderEventLoggers,
1332 ProviderEventLoggers.NoOpProviderEventLoggers,
1333 ),
1334 ),
1335 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1303 Layer.provideMerge(NodeServices.layer),1336 Layer.provideMerge(NodeServices.layer),
1304 );1337 );
1305 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(1338 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(
@@ -1307,7 +1340,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 1 unmodified line
1307 );1340 );
1308 1341
1309 yield* Effect.gen(function* () {1342 yield* Effect.gen(function* () {
1310 const registry = yield* ProviderRegistry;1343 const registry = yield* ProviderRegistry.ProviderRegistry;
1311 const providers = yield* registry.getProviders;1344 const providers = yield* registry.getProviders;
1312 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");1345 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");
1313 1346
@@ -1345,15 +1378,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 31 unmodified lines
1345 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1378 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1346 const providerRegistryLayer = ProviderRegistryLive.pipe(1379 const providerRegistryLayer = ProviderRegistryLive.pipe(
1347 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1380 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1348 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1381 Layer.provideMerge(
1382 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1383 ),
1349 Layer.provideMerge(1384 Layer.provideMerge(
1350 ServerConfig.layerTest(process.cwd(), {1385 ServerConfig.layerTest(process.cwd(), {
1351 prefix: "t3-provider-registry-",1386 prefix: "t3-provider-registry-",
1352 }),1387 }),
1353 ),1388 ),
1354 Layer.provideMerge(TestHttpClientLive),1389 Layer.provideMerge(TestHttpClientLive),
1355 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1390 Layer.provideMerge(
1356 Layer.provideMerge(OpenCodeRuntimeLive),1391 Layer.succeed(
1392 ProviderEventLoggers.ProviderEventLoggers,
1393 ProviderEventLoggers.NoOpProviderEventLoggers,
1394 ),
1395 ),
1396 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1357 Layer.provideMerge(1397 Layer.provideMerge(
1358 mockCommandSpawnerLayer((command, args) => {1398 mockCommandSpawnerLayer((command, args) => {
1359 if (command === "agent") {1399 if (command === "agent") {
@@ -1380,13 +1420,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T · 20 unmodified lines
1380 );1420 );
1381 const runtimeServices = yield* Layer.build(1421 const runtimeServices = yield* Layer.build(
1382 Layer.mergeAll(1422 Layer.mergeAll(
1383 Layer.succeed(ServerSettingsService, serverSettings),1423 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1384 providerRegistryLayer,1424 providerRegistryLayer,
1385 ),1425 ),
1386 ).pipe(Scope.provide(scope));1426 ).pipe(Scope.provide(scope));
1387 1427
1388 yield* Effect.gen(function* () {1428 yield* Effect.gen(function* () {
1389 const registry = yield* ProviderRegistry;1429 const registry = yield* ProviderRegistry.ProviderRegistry;
1390 const providers = yield* registry.getProviders;1430 const providers = yield* registry.getProviders;
1391 const cursorProvider = providers.find(1431 const cursorProvider = providers.find(
1392 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),1432 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
apps/server/src/provider/providerUpdateSettings.ts+2−2
@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines
3import * as Equal from "effect/Equal";3import * as Equal from "effect/Equal";
4import * as Stream from "effect/Stream";4import * as Stream from "effect/Stream";
5 5
6import type { ServerSettingsShape } from "../serverSettings.ts";6import type * as ServerSettingsModule from "../serverSettings.ts";
7 7
8export interface ProviderSnapshotSettings<Settings> {8export interface ProviderSnapshotSettings<Settings> {
9 readonly provider: Settings;9 readonly provider: Settings;
@@ -29,7 +29,7 @@ export function haveProviderSnapshotSettingsChanged<Settings>( · 19 unmodified lines
29 29
30export function makeProviderSnapshotSettingsSource<Settings>(30export function makeProviderSnapshotSettingsSource<Settings>(
31 provider: Settings,31 provider: Settings,
32 serverSettings: ServerSettingsShape,32 serverSettings: ServerSettingsModule.ServerSettingsService["Service"],
33): {33): {
34 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;34 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;
35 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;35 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;
apps/server/src/server.test.ts+95−100
@@ -69,56 +69,30 @@ import { vi } from "vite-plus/test"; · 68 unmodified lines
69 69
70const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");70const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
71 71
72import type { ServerConfigShape } from "./config.ts";72import * as ServerConfig from "./config.ts";
73import { deriveServerPaths, ServerConfig } from "./config.ts";
74import { makeRoutesLayer } from "./server.ts";73import { makeRoutesLayer } from "./server.ts";
75import {74import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts";
76 CheckpointDiffQuery,75import * as GitManager from "./git/GitManager.ts";
77 type CheckpointDiffQueryShape,76import * as Keybindings from "./keybindings.ts";
78} from "./checkpointing/Services/CheckpointDiffQuery.ts";
79import { GitManager, type GitManagerShape } from "./git/GitManager.ts";
80import { Keybindings, type KeybindingsShape } from "./keybindings.ts";
81import * as ExternalLauncher from "./process/externalLauncher.ts";77import * as ExternalLauncher from "./process/externalLauncher.ts";
82import {78import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
83 OrchestrationEngineService,
84 type OrchestrationEngineShape,
85} from "./orchestration/Services/OrchestrationEngine.ts";
86import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts";79import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts";
87import {80import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
88 ProjectionSnapshotQuery,
89 type ProjectionSnapshotQueryShape,
90} from "./orchestration/Services/ProjectionSnapshotQuery.ts";
91import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";81import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";
92import { PersistenceSqlError } from "./persistence/Errors.ts";82import { PersistenceSqlError } from "./persistence/Errors.ts";
93import {83import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts";
94 ProviderRegistry,
95 type ProviderRegistryShape,
96} from "./provider/Services/ProviderRegistry.ts";
97import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts";84import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts";
98import { ServerLifecycleEvents, type ServerLifecycleEventsShape } from "./serverLifecycleEvents.ts";85import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
99import { ServerRuntimeStartup, type ServerRuntimeStartupShape } from "./serverRuntimeStartup.ts";86import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
100import { ServerSettingsService, type ServerSettingsShape } from "./serverSettings.ts";87import * as ServerSettings from "./serverSettings.ts";
101import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager.ts";88import * as TerminalManager from "./terminal/Services/Manager.ts";
102import * as PreviewManager from "./preview/Manager.ts";89import * as PreviewManager from "./preview/Manager.ts";
103import * as PortScanner from "./preview/PortScanner.ts";90import * as PortScanner from "./preview/PortScanner.ts";
104import {91import * as BrowserTraceCollector from "./observability/Services/BrowserTraceCollector.ts";
105 BrowserTraceCollector,
106 type BrowserTraceCollectorShape,
107} from "./observability/Services/BrowserTraceCollector.ts";
108import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";92import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
109import {93import * as ProjectSetupScriptRunner from "./project/Services/ProjectSetupScriptRunner.ts";
110 ProjectSetupScriptRunner,94import * as RepositoryIdentityResolver from "./project/Services/RepositoryIdentityResolver.ts";
111 ProjectSetupScriptRunnerError,95import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
112 type ProjectSetupScriptRunnerShape,
113} from "./project/Services/ProjectSetupScriptRunner.ts";
114import {
115 RepositoryIdentityResolver,
116 type RepositoryIdentityResolverShape,
117} from "./project/Services/RepositoryIdentityResolver.ts";
118import {
119 ServerEnvironment,
120 type ServerEnvironmentShape,
121} from "./environment/Services/ServerEnvironment.ts";
122import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";96import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
123import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";97import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";
124import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";98import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
@@ -132,10 +106,7 @@ import * as ReviewService from "./review/ReviewService.ts"; · 7 unmodified lines
132import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";106import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";
133import * as ServerSecretStore from "./auth/ServerSecretStore.ts";107import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
134import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";108import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
135import {109import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts";
136 CloudManagedEndpointRuntime,
137 type CloudManagedEndpointRuntimeShape,
138} from "./cloud/ManagedEndpointRuntime.ts";
139import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts";110import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts";
140import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";111import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
141import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";112import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
@@ -341,32 +312,40 @@ const makeBrowserOtlpPayload = (spanName: string) => · 199 unmodified lines
341 });312 });
342 313
343const buildAppUnderTest = (options?: {314const buildAppUnderTest = (options?: {
344 config?: Partial<ServerConfigShape>;315 config?: Partial<ServerConfig.ServerConfig["Service"]>;
345 layers?: {316 layers?: {
346 keybindings?: Partial<KeybindingsShape>;317 keybindings?: Partial<Keybindings.Keybindings["Service"]>;
347 providerRegistry?: Partial<ProviderRegistryShape>;318 providerRegistry?: Partial<ProviderRegistry.ProviderRegistry["Service"]>;
348 serverSettings?: Partial<ServerSettingsShape>;319 serverSettings?: Partial<ServerSettings.ServerSettingsService["Service"]>;
349 externalLauncher?: Partial<ExternalLauncher.ExternalLauncherShape>;320 externalLauncher?: Partial<ExternalLauncher.ExternalLauncher["Service"]>;
350 vcsDriver?: Partial<VcsDriver.VcsDriverShape>;321 vcsDriver?: Partial<VcsDriver.VcsDriver["Service"]>;
351 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistryShape>;322 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistry["Service"]>;
352 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriverShape>;323 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
353 gitManager?: Partial<GitManagerShape>;324 gitManager?: Partial<GitManager.GitManager["Service"]>;
354 sourceControlRepositoryService?: Partial<SourceControlRepositoryService.SourceControlRepositoryServiceShape>;325 sourceControlRepositoryService?: Partial<
355 reviewService?: Partial<ReviewService.ReviewServiceShape>;326 SourceControlRepositoryService.SourceControlRepositoryService["Service"]
356 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcasterShape>;327 >;
357 projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;328 reviewService?: Partial<ReviewService.ReviewService["Service"]>;
358 terminalManager?: Partial<TerminalManagerShape>;329 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcaster["Service"]>;
359 orchestrationEngine?: Partial<OrchestrationEngineShape>;330 projectSetupScriptRunner?: Partial<
360 projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>;331 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]
361 checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>;332 >;
362 browserTraceCollector?: Partial<BrowserTraceCollectorShape>;333 terminalManager?: Partial<TerminalManager.TerminalManager["Service"]>;
363 serverLifecycleEvents?: Partial<ServerLifecycleEventsShape>;334 orchestrationEngine?: Partial<OrchestrationEngine.OrchestrationEngineService["Service"]>;
364 serverRuntimeStartup?: Partial<ServerRuntimeStartupShape>;335 projectionSnapshotQuery?: Partial<ProjectionSnapshotQuery.ProjectionSnapshotQuery["Service"]>;
365 serverEnvironment?: Partial<ServerEnvironmentShape>;336 checkpointDiffQuery?: Partial<CheckpointDiffQuery.CheckpointDiffQuery["Service"]>;
366 repositoryIdentityResolver?: Partial<RepositoryIdentityResolverShape>;337 browserTraceCollector?: Partial<BrowserTraceCollector.BrowserTraceCollector["Service"]>;
367 cloudManagedEndpointRuntime?: Partial<CloudManagedEndpointRuntimeShape>;338 serverLifecycleEvents?: Partial<ServerLifecycleEvents.ServerLifecycleEvents["Service"]>;
368 relayClient?: Partial<RelayClient.RelayClientShape>;339 serverRuntimeStartup?: Partial<ServerRuntimeStartup.ServerRuntimeStartup["Service"]>;
369 cloudCliTokenManager?: Partial<CloudCliTokenManager.CloudCliTokenManagerShape>;340 serverEnvironment?: Partial<ServerEnvironment.ServerEnvironment["Service"]>;
341 repositoryIdentityResolver?: Partial<
342 RepositoryIdentityResolver.RepositoryIdentityResolver["Service"]
343 >;
344 cloudManagedEndpointRuntime?: Partial<
345 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]
346 >;
347 relayClient?: Partial<RelayClient.RelayClient["Service"]>;
348 cloudCliTokenManager?: Partial<CloudCliTokenManager.CloudCliTokenManager["Service"]>;
370 };349 };
371}) =>350}) =>
372 Effect.gen(function* () {351 Effect.gen(function* () {
@@ -374,8 +353,8 @@ const buildAppUnderTest = (options?: { · 1 unmodified line
374 const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });353 const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });
375 const baseDir = options?.config?.baseDir ?? tempBaseDir;354 const baseDir = options?.config?.baseDir ?? tempBaseDir;
376 const devUrl = options?.config?.devUrl;355 const devUrl = options?.config?.devUrl;
377 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);356 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
378 const config: ServerConfigShape = {357 const config: ServerConfig.ServerConfig["Service"] = {
379 logLevel: "Info",358 logLevel: "Info",
380 traceMinLevel: "Info",359 traceMinLevel: "Info",
381 traceTimingEnabled: true,360 traceTimingEnabled: true,
@@ -403,8 +382,8 @@ const buildAppUnderTest = (options?: { · 21 unmodified lines
403 tailscaleServePort: 443,382 tailscaleServePort: 443,
404 ...options?.config,383 ...options?.config,
405 };384 };
406 const layerConfig = Layer.succeed(ServerConfig, config);385 const layerConfig = ServerConfig.layer(config);
407 const defaultVcsDriver: VcsDriver.VcsDriverShape = {386 const defaultVcsDriver: VcsDriver.VcsDriver["Service"] = {
408 capabilities: {387 capabilities: {
409 kind: "git",388 kind: "git",
410 supportsWorktrees: true,389 supportsWorktrees: true,
@@ -502,7 +481,7 @@ const buildAppUnderTest = (options?: { · 91 unmodified lines
502 const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({481 const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({
503 ...options?.layers?.gitVcsDriver,482 ...options?.layers?.gitVcsDriver,
504 });483 });
505 const gitManagerLayer = Layer.mock(GitManager)({484 const gitManagerLayer = Layer.mock(GitManager.GitManager)({
506 ...options?.layers?.gitManager,485 ...options?.layers?.gitManager,
507 });486 });
508 const workspaceEntriesLayer = WorkspaceEntries.layer.pipe(487 const workspaceEntriesLayer = WorkspaceEntries.layer.pipe(
@@ -545,7 +524,7 @@ const buildAppUnderTest = (options?: { · 36 unmodified lines
545 disableLogger: true,524 disableLogger: true,
546 }).pipe(525 }).pipe(
547 Layer.provide(526 Layer.provide(
548 Layer.mock(Keybindings)({527 Layer.mock(Keybindings.Keybindings)({
549 loadConfigState: Effect.succeed({528 loadConfigState: Effect.succeed({
550 keybindings: [],529 keybindings: [],
551 issues: [],530 issues: [],
@@ -555,7 +534,7 @@ const buildAppUnderTest = (options?: { · 3 unmodified lines
555 }),534 }),
556 ),535 ),
557 Layer.provide(536 Layer.provide(
558 Layer.mock(ProviderRegistry)({537 Layer.mock(ProviderRegistry.ProviderRegistry)({
559 getProviders: Effect.succeed([]),538 getProviders: Effect.succeed([]),
560 refresh: () => Effect.succeed([]),539 refresh: () => Effect.succeed([]),
561 refreshInstance: () => Effect.succeed([]),540 refreshInstance: () => Effect.succeed([]),
@@ -569,7 +548,7 @@ const buildAppUnderTest = (options?: { · 7 unmodified lines
569 }),548 }),
570 ),549 ),
571 Layer.provide(550 Layer.provide(
572 Layer.mock(ServerSettingsService)({551 Layer.mock(ServerSettings.ServerSettingsService)({
573 start: Effect.void,552 start: Effect.void,
574 ready: Effect.void,553 ready: Effect.void,
575 getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),554 getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),
@@ -658,13 +637,13 @@ const buildAppUnderTest = (options?: { · 82 unmodified lines
658 ),637 ),
659 Layer.provideMerge(vcsStatusBroadcasterLayer),638 Layer.provideMerge(vcsStatusBroadcasterLayer),
660 Layer.provide(639 Layer.provide(
661 Layer.mock(ProjectSetupScriptRunner)({640 Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({
662 runForThread: () => Effect.succeed({ status: "no-script" as const }),641 runForThread: () => Effect.succeed({ status: "no-script" as const }),
663 ...options?.layers?.projectSetupScriptRunner,642 ...options?.layers?.projectSetupScriptRunner,
664 }),643 }),
665 ),644 ),
666 Layer.provide(645 Layer.provide(
667 Layer.mock(TerminalManager)({646 Layer.mock(TerminalManager.TerminalManager)({
668 ...options?.layers?.terminalManager,647 ...options?.layers?.terminalManager,
669 }),648 }),
670 ),649 ),
@@ -692,7 +671,7 @@ const buildAppUnderTest = (options?: { · 21 unmodified lines
692 ),671 ),
693 ),672 ),
694 Layer.provide(673 Layer.provide(
695 Layer.mock(OrchestrationEngineService)({674 Layer.mock(OrchestrationEngine.OrchestrationEngineService)({
696 readEvents: () => Stream.empty,675 readEvents: () => Stream.empty,
697 dispatch: () => Effect.succeed({ sequence: 0 }),676 dispatch: () => Effect.succeed({ sequence: 0 }),
698 streamDomainEvents: Stream.empty,677 streamDomainEvents: Stream.empty,
@@ -700,7 +679,7 @@ const buildAppUnderTest = (options?: { · 1 unmodified line
700 }),679 }),
701 ),680 ),
702 Layer.provide(681 Layer.provide(
703 Layer.mock(ProjectionSnapshotQuery)({682 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({
704 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),683 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
705 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),684 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
706 getShellSnapshot: () =>685 getShellSnapshot: () =>
@@ -729,7 +708,7 @@ const buildAppUnderTest = (options?: { · 22 unmodified lines
729 }),708 }),
730 ),709 ),
731 Layer.provide(710 Layer.provide(
732 Layer.mock(CheckpointDiffQuery)({711 Layer.mock(CheckpointDiffQuery.CheckpointDiffQuery)({
733 getTurnDiff: () =>712 getTurnDiff: () =>
734 Effect.succeed({713 Effect.succeed({
735 threadId: defaultThreadId,714 threadId: defaultThreadId,
@@ -751,13 +730,13 @@ const buildAppUnderTest = (options?: { · 15 unmodified lines
751 730
752 const appLayer = servedRoutesLayer.pipe(731 const appLayer = servedRoutesLayer.pipe(
753 Layer.provide(732 Layer.provide(
754 Layer.mock(BrowserTraceCollector)({733 Layer.mock(BrowserTraceCollector.BrowserTraceCollector)({
755 record: () => Effect.void,734 record: () => Effect.void,
756 ...options?.layers?.browserTraceCollector,735 ...options?.layers?.browserTraceCollector,
757 }),736 }),
758 ),737 ),
759 Layer.provide(738 Layer.provide(
760 Layer.mock(ServerLifecycleEvents)({739 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
761 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),740 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),
762 snapshot: Effect.succeed({ sequence: 0, events: [] }),741 snapshot: Effect.succeed({ sequence: 0, events: [] }),
763 stream: Stream.empty,742 stream: Stream.empty,
@@ -765,7 +744,7 @@ const buildAppUnderTest = (options?: { · 1 unmodified line
765 }),744 }),
766 ),745 ),
767 Layer.provide(746 Layer.provide(
768 Layer.mock(ServerRuntimeStartup)({747 Layer.mock(ServerRuntimeStartup.ServerRuntimeStartup)({
769 awaitCommandReady: Effect.void,748 awaitCommandReady: Effect.void,
770 markHttpListening: Effect.void,749 markHttpListening: Effect.void,
771 enqueueCommand: (effect) => effect,750 enqueueCommand: (effect) => effect,
@@ -773,22 +752,22 @@ const buildAppUnderTest = (options?: { · 1 unmodified line
773 }),752 }),
774 ),753 ),
775 Layer.provide(754 Layer.provide(
776 Layer.mock(ServerEnvironment)({755 Layer.mock(ServerEnvironment.ServerEnvironment)({
777 getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),756 getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),
778 getDescriptor: Effect.succeed(testEnvironmentDescriptor),757 getDescriptor: Effect.succeed(testEnvironmentDescriptor),
779 ...options?.layers?.serverEnvironment,758 ...options?.layers?.serverEnvironment,
780 }),759 }),
781 ),760 ),
782 Layer.provide(761 Layer.provide(
783 Layer.mock(RepositoryIdentityResolver)({762 Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({
784 resolve: () => Effect.succeed(null),763 resolve: () => Effect.succeed(null),
785 ...options?.layers?.repositoryIdentityResolver,764 ...options?.layers?.repositoryIdentityResolver,
786 }),765 }),
787 ),766 ),
788 Layer.provide(767 Layer.provide(
789 Layer.succeed(768 Layer.succeed(
790 CloudManagedEndpointRuntime,769 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime,
791 CloudManagedEndpointRuntime.of({770 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
792 applyConfig: () => Effect.succeed({ status: "disabled" }),771 applyConfig: () => Effect.succeed({ status: "disabled" }),
793 ...options?.layers?.cloudManagedEndpointRuntime,772 ...options?.layers?.cloudManagedEndpointRuntime,
794 }),773 }),
@@ -5939,14 +5918,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 5144 unmodified lines
5939 }),5918 }),
5940 );5919 );
5941 const fetchRemote = vi.fn(5920 const fetchRemote = vi.fn(
5942 (_: Parameters<GitVcsDriver.GitVcsDriverShape["fetchRemote"]>[0]) =>5921 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
5943 Effect.sync(() => {5922 Effect.sync(() => {
5944 bootstrapGitOperations.push("fetch");5923 bootstrapGitOperations.push("fetch");
5945 }),5924 }),
5946 );5925 );
5947 const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";5926 const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";
5948 const resolveRemoteTrackingCommit = vi.fn(5927 const resolveRemoteTrackingCommit = vi.fn(
5949 (_: Parameters<GitVcsDriver.GitVcsDriverShape["resolveRemoteTrackingCommit"]>[0]) =>5928 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
5950 Effect.sync(() => {5929 Effect.sync(() => {
5951 bootstrapGitOperations.push("resolve-remote-commit");5930 bootstrapGitOperations.push("resolve-remote-commit");
5952 return {5931 return {
@@ -5956,7 +5935,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 3 unmodified lines
5956 }),5935 }),
5957 );5936 );
5958 const createWorktree = vi.fn(5937 const createWorktree = vi.fn(
5959 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>5938 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
5960 Effect.sync(() => {5939 Effect.sync(() => {
5961 bootstrapGitOperations.push("create-worktree");5940 bootstrapGitOperations.push("create-worktree");
5962 return {5941 return {
@@ -5968,7 +5947,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 5 unmodified lines
5968 }),5947 }),
5969 );5948 );
5970 const runForThread = vi.fn(5949 const runForThread = vi.fn(
5971 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>5950 (
5951 _: Parameters<
5952 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
5953 >[0],
5954 ) =>
5972 Effect.succeed({5955 Effect.succeed({
5973 status: "started" as const,5956 status: "started" as const,
5974 scriptId: "setup",5957 scriptId: "setup",
@@ -6102,7 +6085,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 127 unmodified lines
6102 Effect.gen(function* () {6085 Effect.gen(function* () {
6103 const dispatchedCommands: Array<OrchestrationCommand> = [];6086 const dispatchedCommands: Array<OrchestrationCommand> = [];
6104 const createWorktree = vi.fn(6087 const createWorktree = vi.fn(
6105 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6088 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6106 Effect.succeed({6089 Effect.succeed({
6107 worktree: {6090 worktree: {
6108 refName: "t3code/bootstrap-refName",6091 refName: "t3code/bootstrap-refName",
@@ -6111,8 +6094,16 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 2 unmodified lines
6111 }),6094 }),
6112 );6095 );
6113 const runForThread = vi.fn(6096 const runForThread = vi.fn(
6114 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>6097 (
6115 Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })),6098 _: Parameters<
6099 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6100 >[0],
6101 ) =>
6102 Effect.fail(
6103 new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({
6104 message: "pty unavailable",
6105 }),
6106 ),
6116 );6107 );
6117 6108
6118 yield* buildAppUnderTest({6109 yield* buildAppUnderTest({
@@ -6196,7 +6187,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 77 unmodified lines
6196 Effect.gen(function* () {6187 Effect.gen(function* () {
6197 const dispatchedCommands: Array<OrchestrationCommand> = [];6188 const dispatchedCommands: Array<OrchestrationCommand> = [];
6198 const createWorktree = vi.fn(6189 const createWorktree = vi.fn(
6199 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6190 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6200 Effect.succeed({6191 Effect.succeed({
6201 worktree: {6192 worktree: {
6202 refName: "t3code/bootstrap-refName",6193 refName: "t3code/bootstrap-refName",
@@ -6205,7 +6196,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 2 unmodified lines
6205 }),6196 }),
6206 );6197 );
6207 const runForThread = vi.fn(6198 const runForThread = vi.fn(
6208 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>6199 (
6200 _: Parameters<
6201 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6202 >[0],
6203 ) =>
6209 Effect.succeed({6204 Effect.succeed({
6210 status: "started" as const,6205 status: "started" as const,
6211 scriptId: "setup",6206 scriptId: "setup",
@@ -6315,7 +6310,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 103 unmodified lines
6315 Effect.gen(function* () {6310 Effect.gen(function* () {
6316 const dispatchedCommands: Array<OrchestrationCommand> = [];6311 const dispatchedCommands: Array<OrchestrationCommand> = [];
6317 const createWorktree = vi.fn(6312 const createWorktree = vi.fn(
6318 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6313 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6319 Effect.die(new Error("worktree exploded")),6314 Effect.die(new Error("worktree exploded")),
6320 );6315 );
6321 6316
apps/server/src/server.ts+17−17
@@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; · 3 unmodified lines
4import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";4import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
5import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";5import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
6 6
7import { ServerConfig } from "./config.ts";7import * as ServerConfig from "./config.ts";
8import {8import {
9 otlpTracesProxyRouteLayer,9 otlpTracesProxyRouteLayer,
10 assetRouteLayer,10 assetRouteLayer,
@@ -16,15 +16,15 @@ import { fixPath } from "./os-jank.ts"; · 5 unmodified lines
16import { websocketRpcRouteLayer } from "./ws.ts";16import { websocketRpcRouteLayer } from "./ws.ts";
17import * as ExternalLauncher from "./process/externalLauncher.ts";17import * as ExternalLauncher from "./process/externalLauncher.ts";
18import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";18import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
19import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";19import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
22import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";22import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";
23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
24import { ProviderEventLoggersLive } from "./provider/Layers/ProviderEventLoggers.ts";24import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
25import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";25import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";
26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
27import { OpenCodeRuntimeLive } from "./provider/opencodeRuntime.ts";27import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";
28import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";28import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
29import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";29import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
30import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";30import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
@@ -40,8 +40,8 @@ import * as PreviewManager from "./preview/Manager.ts"; · 9 unmodified lines
40import * as PortScanner from "./preview/PortScanner.ts";40import * as PortScanner from "./preview/PortScanner.ts";
41import * as ProcessRunner from "./processRunner.ts";41import * as ProcessRunner from "./processRunner.ts";
42import * as GitManager from "./git/GitManager.ts";42import * as GitManager from "./git/GitManager.ts";
43import { KeybindingsLive } from "./keybindings.ts";43import * as Keybindings from "./keybindings.ts";
44import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";44import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
45import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";45import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
46import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";46import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
47import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";47import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
@@ -51,7 +51,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion · 3 unmodified lines
51import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";51import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
52import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";52import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
53import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";53import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";
54import { ServerSettingsLive } from "./serverSettings.ts";54import * as ServerSettings from "./serverSettings.ts";
55import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";55import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
56import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";56import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
57import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";57import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
@@ -112,14 +112,14 @@ const PtyAdapterLive = Layer.unwrap( · 54 unmodified lines
112 112
113const RelayClientLive = Layer.unwrap(113const RelayClientLive = Layer.unwrap(
114 Effect.gen(function* () {114 Effect.gen(function* () {
115 const config = yield* ServerConfig;115 const config = yield* ServerConfig.ServerConfig;
116 return RelayClient.layerCloudflared({ baseDir: config.baseDir });116 return RelayClient.layerCloudflared({ baseDir: config.baseDir });
117 }),117 }),
118);118);
119 119
120const HttpServerLive = Layer.unwrap(120const HttpServerLive = Layer.unwrap(
121 Effect.gen(function* () {121 Effect.gen(function* () {
122 const config = yield* ServerConfig;122 const config = yield* ServerConfig.ServerConfig;
123 if (typeof Bun !== "undefined") {123 if (typeof Bun !== "undefined") {
124 const BunHttpServer = yield* Effect.promise(124 const BunHttpServer = yield* Effect.promise(
125 () => import("@effect/platform-bun/BunHttpServer"),125 () => import("@effect/platform-bun/BunHttpServer"),
@@ -292,7 +292,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( · 166 unmodified lines
292 Layer.provideMerge(ProviderRuntimeLayerLive),292 Layer.provideMerge(ProviderRuntimeLayerLive),
293 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),293 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),
294 Layer.provideMerge(PersistenceLayerLive),294 Layer.provideMerge(PersistenceLayerLive),
295 Layer.provideMerge(KeybindingsLive),295 Layer.provideMerge(Keybindings.layer),
296 Layer.provideMerge(ProviderRegistryLive),296 Layer.provideMerge(ProviderRegistryLive),
297 // The instance registry is the new routing keystone — text generation,297 // The instance registry is the new routing keystone — text generation,
298 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`298 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
@@ -305,14 +305,14 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( · 6 unmodified lines
305 // `ProviderService` (canonical stream, written after event normalization).305 // `ProviderService` (canonical stream, written after event normalization).
306 // Provided once at the runtime level so every consumer sees the same306 // Provided once at the runtime level so every consumer sees the same
307 // logger instances.307 // logger instances.
308 Layer.provideMerge(ProviderEventLoggersLive),308 Layer.provideMerge(ProviderEventLoggers.ProviderEventLoggersLive),
309 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old309 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old
310 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but310 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but
311 // the rewritten registry reads snapshots off the instance registry and311 // the rewritten registry reads snapshots off the instance registry and
312 // no longer transitively provides it. Exposing it at the runtime level312 // no longer transitively provides it. Exposing it at the runtime level
313 // keeps a single Live for all opencode consumers.313 // keeps a single Live for all opencode consumers.
314 Layer.provideMerge(OpenCodeRuntimeLive),314 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
315 Layer.provideMerge(ServerSettingsLive),315 Layer.provideMerge(ServerSettings.layer.pipe(Layer.provide(ServerSecretStore.layer))),
316 Layer.provideMerge(WorkspaceLayerLive),316 Layer.provideMerge(WorkspaceLayerLive),
317 Layer.provideMerge(ProjectFaviconResolverLayerLive),317 Layer.provideMerge(ProjectFaviconResolverLayerLive),
318 Layer.provideMerge(RepositoryIdentityResolverLive),318 Layer.provideMerge(RepositoryIdentityResolverLive),
@@ -334,11 +334,11 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe( · 15 unmodified lines
334 Layer.provideMerge(TraceDiagnostics.layer),334 Layer.provideMerge(TraceDiagnostics.layer),
335 Layer.provideMerge(AnalyticsServiceLayerLive),335 Layer.provideMerge(AnalyticsServiceLayerLive),
336 Layer.provideMerge(ExternalLauncher.layer),336 Layer.provideMerge(ExternalLauncher.layer),
337 Layer.provideMerge(ServerLifecycleEventsLive),337 Layer.provideMerge(ServerLifecycleEvents.layer),
338 Layer.provide(NetService.layer),338 Layer.provide(NetService.layer),
339);339);
340 340
341const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(341const RuntimeServicesLive = ServerRuntimeStartup.layer.pipe(
342 Layer.provideMerge(RuntimeDependenciesLive),342 Layer.provideMerge(RuntimeDependenciesLive),
343);343);
344 344
@@ -361,14 +361,14 @@ export const makeRoutesLayer = Layer.mergeAll( · 16 unmodified lines
361 361
362export const makeServerLayer = Layer.unwrap(362export const makeServerLayer = Layer.unwrap(
363 Effect.gen(function* () {363 Effect.gen(function* () {
364 const config = yield* ServerConfig;364 const config = yield* ServerConfig.ServerConfig;
365 365
366 yield* fixPath();366 yield* fixPath();
367 367
368 const httpListeningLayer = Layer.effectDiscard(368 const httpListeningLayer = Layer.effectDiscard(
369 Effect.gen(function* () {369 Effect.gen(function* () {
370 yield* HttpServer.HttpServer;370 yield* HttpServer.HttpServer;
371 const startup = yield* ServerRuntimeStartup;371 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
372 yield* startup.markHttpListening;372 yield* startup.markHttpListening;
373 }),373 }),
374 );374 );
apps/server/src/serverLifecycleEvents.test.ts+3−3
@@ -4,13 +4,13 @@ import { assertTrue } from "@effect/vitest/utils"; · 3 unmodified lines
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6 6
7import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";7import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
8 8
9it.effect(9it.effect(
10 "publishes lifecycle events without subscribers and snapshots the latest welcome/ready",10 "publishes lifecycle events without subscribers and snapshots the latest welcome/ready",
11 () =>11 () =>
12 Effect.gen(function* () {12 Effect.gen(function* () {
13 const lifecycleEvents = yield* ServerLifecycleEvents;13 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
14 const environment = {14 const environment = {
15 environmentId: EnvironmentId.make("environment-test"),15 environmentId: EnvironmentId.make("environment-test"),
16 label: "Test environment",16 label: "Test environment",
@@ -49,5 +49,5 @@ it.effect( · 32 unmodified lines
49 const snapshot = yield* lifecycleEvents.snapshot;49 const snapshot = yield* lifecycleEvents.snapshot;
50 assert.equal(snapshot.sequence, 2);50 assert.equal(snapshot.sequence, 2);
51 assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]);51 assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]);
52 }).pipe(Effect.provide(ServerLifecycleEventsLive)),52 }).pipe(Effect.provide(ServerLifecycleEvents.layer)),
53);53);
apps/server/src/serverLifecycleEvents.ts+34−37
@@ -1,9 +1,9 @@
1import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";1import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";
2import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
4import * as PubSub from "effect/PubSub";5import * as PubSub from "effect/PubSub";
5import * as Ref from "effect/Ref";6import * as Ref from "effect/Ref";
6import * as Context from "effect/Context";
7import * as Stream from "effect/Stream";7import * as Stream from "effect/Stream";
8 8
9type LifecycleEventInput =9type LifecycleEventInput =
@@ -15,44 +15,41 @@ interface SnapshotState { · 5 unmodified lines
15 readonly events: ReadonlyArray<ServerLifecycleStreamEvent>;15 readonly events: ReadonlyArray<ServerLifecycleStreamEvent>;
16}16}
17 17
18export interface ServerLifecycleEventsShape {
19 readonly publish: (event: LifecycleEventInput) => Effect.Effect<ServerLifecycleStreamEvent>;
20 readonly snapshot: Effect.Effect<SnapshotState>;
21 readonly stream: Stream.Stream<ServerLifecycleStreamEvent>;
22}
23
24export class ServerLifecycleEvents extends Context.Service<18export class ServerLifecycleEvents extends Context.Service<
25 ServerLifecycleEvents,19 ServerLifecycleEvents,
26 ServerLifecycleEventsShape20 {
21 readonly publish: (event: LifecycleEventInput) => Effect.Effect<ServerLifecycleStreamEvent>;
22 readonly snapshot: Effect.Effect<SnapshotState>;
23 readonly stream: Stream.Stream<ServerLifecycleStreamEvent>;
24 }
27>()("t3/serverLifecycleEvents") {}25>()("t3/serverLifecycleEvents") {}
28 26
29export const ServerLifecycleEventsLive = Layer.effect(27const make = Effect.gen(function* () {
30 ServerLifecycleEvents,28 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();
31 Effect.gen(function* () {29 const state = yield* Ref.make<SnapshotState>({
32 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();30 sequence: 0,
33 const state = yield* Ref.make<SnapshotState>({31 events: [],
34 sequence: 0,32 });
35 events: [],33
36 });34 return {
35 publish: (event) =>
36 Ref.modify(state, (current) => {
37 const nextSequence = current.sequence + 1;
38 const nextEvent = {
39 ...event,
40 sequence: nextSequence,
41 } satisfies ServerLifecycleStreamEvent;
42 const nextEvents =
43 nextEvent.type === "welcome"
44 ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")]
45 : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")];
46 return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const;
47 }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))),
48 snapshot: Ref.get(state),
49 get stream() {
50 return Stream.fromPubSub(pubsub);
51 },
52 } satisfies ServerLifecycleEvents["Service"];
53});
37 54
38 return {55export const layer = Layer.effect(ServerLifecycleEvents, make);
39 publish: (event) =>
40 Ref.modify(state, (current) => {
41 const nextSequence = current.sequence + 1;
42 const nextEvent = {
43 ...event,
44 sequence: nextSequence,
45 } satisfies ServerLifecycleStreamEvent;
46 const nextEvents =
47 nextEvent.type === "welcome"
48 ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")]
49 : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")];
50 return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const;
51 }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))),
52 snapshot: Ref.get(state),
53 get stream() {
54 return Stream.fromPubSub(pubsub);
55 },
56 } satisfies ServerLifecycleEventsShape;
57 }),
58);
apps/server/src/serverRuntimeStartup.test.ts+32−42
@@ -10,24 +10,14 @@ import * as PlatformError from "effect/PlatformError"; · 9 unmodified lines
10import * as Ref from "effect/Ref";10import * as Ref from "effect/Ref";
11import * as Stream from "effect/Stream";11import * as Stream from "effect/Stream";
12 12
13import { ServerConfig } from "./config.ts";13import * as ServerConfig from "./config.ts";
14import {14import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
15 OrchestrationEngineService,15import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
16 type OrchestrationEngineShape,16import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
17} from "./orchestration/Services/OrchestrationEngine.ts";17import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
18import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
19import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
20import {
21 getAutoBootstrapDefaultModelSelection,
22 launchStartupHeartbeat,
23 makeCommandGate,
24 resolveAutoBootstrapWelcomeTargets,
25 resolveWelcomeBase,
26 ServerRuntimeStartupError,
27} from "./serverRuntimeStartup.ts";
28 18
29it("uses the canonical Codex default for auto-bootstrapped model selection", () => {19it("uses the canonical Codex default for auto-bootstrapped model selection", () => {
30 assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), {20 assert.deepStrictEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), {
31 instanceId: ProviderInstanceId.make("codex"),21 instanceId: ProviderInstanceId.make("codex"),
32 model: DEFAULT_MODEL,22 model: DEFAULT_MODEL,
33 });23 });
@@ -37,7 +27,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () = · 3 unmodified lines
37 Effect.scoped(27 Effect.scoped(
38 Effect.gen(function* () {28 Effect.gen(function* () {
39 const executionCount = yield* Ref.make(0);29 const executionCount = yield* Ref.make(0);
40 const commandGate = yield* makeCommandGate;30 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
41 31
42 const queuedCommandFiber = yield* commandGate32 const queuedCommandFiber = yield* commandGate
43 .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1))33 .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1))
@@ -58,7 +48,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () = · 14 unmodified lines
58it.effect("enqueueCommand fails queued work when readiness fails", () =>48it.effect("enqueueCommand fails queued work when readiness fails", () =>
59 Effect.scoped(49 Effect.scoped(
60 Effect.gen(function* () {50 Effect.gen(function* () {
61 const commandGate = yield* makeCommandGate;51 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
62 const failure = yield* Deferred.make<void, never>();52 const failure = yield* Deferred.make<void, never>();
63 53
64 const queuedCommandFiber = yield* commandGate54 const queuedCommandFiber = yield* commandGate
@@ -66,13 +56,13 @@ it.effect("enqueueCommand fails queued work when readiness fails", () => · 1 unmodified line
66 .pipe(Effect.forkScoped);56 .pipe(Effect.forkScoped);
67 57
68 yield* commandGate.failCommandReady(58 yield* commandGate.failCommandReady(
69 new ServerRuntimeStartupError({59 new ServerRuntimeStartup.ServerRuntimeStartupError({
70 message: "startup failed",60 stage: "command-readiness",
71 }),61 }),
72 );62 );
73 63
74 const error = yield* Effect.flip(Fiber.join(queuedCommandFiber));64 const error = yield* Effect.flip(Fiber.join(queuedCommandFiber));
75 assert.equal(error.message, "startup failed");65 assert.equal(error.message, "Server runtime startup failed before command readiness.");
76 }),66 }),
77 ),67 ),
78);68);
@@ -82,8 +72,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa · 3 unmodified lines
82 Effect.gen(function* () {72 Effect.gen(function* () {
83 const releaseCounts = yield* Deferred.make<void, never>();73 const releaseCounts = yield* Deferred.make<void, never>();
84 74
85 yield* launchStartupHeartbeat.pipe(75 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(
86 Effect.provideService(ProjectionSnapshotQuery, {76 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
87 getCommandReadModel: () => Effect.die("unused"),77 getCommandReadModel: () => Effect.die("unused"),
88 getSnapshot: () => Effect.die("unused"),78 getSnapshot: () => Effect.die("unused"),
89 getShellSnapshot: () => Effect.die("unused"),79 getShellSnapshot: () => Effect.die("unused"),
@@ -104,7 +94,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa · 14 unmodified lines
104 getThreadShellById: () => Effect.succeed(Option.none()),94 getThreadShellById: () => Effect.succeed(Option.none()),
105 getThreadDetailById: () => Effect.succeed(Option.none()),95 getThreadDetailById: () => Effect.succeed(Option.none()),
106 }),96 }),
107 Effect.provideService(AnalyticsService, {97 Effect.provideService(AnalyticsService.AnalyticsService, {
108 record: () => Effect.void,98 record: () => Effect.void,
109 flush: Effect.void,99 flush: Effect.void,
110 }),100 }),
@@ -115,8 +105,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa · 4 unmodified lines
115 105
116it.effect("resolveWelcomeBase derives cwd and project name from server config", () =>106it.effect("resolveWelcomeBase derives cwd and project name from server config", () =>
117 Effect.gen(function* () {107 Effect.gen(function* () {
118 const welcome = yield* resolveWelcomeBase.pipe(108 const welcome = yield* ServerRuntimeStartup.resolveWelcomeBase.pipe(
119 Effect.provideService(ServerConfig, {109 Effect.provideService(ServerConfig.ServerConfig, {
120 cwd: "/tmp/startup-project",110 cwd: "/tmp/startup-project",
121 } as never),111 } as never),
122 );112 );
@@ -134,12 +124,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa · 11 unmodified lines
134 124
135 return Effect.gen(function* () {125 return Effect.gen(function* () {
136 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);126 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
137 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(127 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
138 Effect.provideService(ServerConfig, {128 Effect.provideService(ServerConfig.ServerConfig, {
139 cwd: "/tmp/startup-project",129 cwd: "/tmp/startup-project",
140 autoBootstrapProjectFromCwd: true,130 autoBootstrapProjectFromCwd: true,
141 } as never),131 } as never),
142 Effect.provideService(ProjectionSnapshotQuery, {132 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
143 getCommandReadModel: () => Effect.die("unused"),133 getCommandReadModel: () => Effect.die("unused"),
144 getSnapshot: () => Effect.die("unused"),134 getSnapshot: () => Effect.die("unused"),
145 getShellSnapshot: () => Effect.die("unused"),135 getShellSnapshot: () => Effect.die("unused"),
@@ -152,7 +142,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa · 6 unmodified lines
152 id: bootstrapProjectId,142 id: bootstrapProjectId,
153 title: "Startup Project",143 title: "Startup Project",
154 workspaceRoot: "/tmp/startup-project",144 workspaceRoot: "/tmp/startup-project",
155 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),145 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
156 scripts: [],146 scripts: [],
157 createdAt: "2026-01-01T00:00:00.000Z",147 createdAt: "2026-01-01T00:00:00.000Z",
158 updatedAt: "2026-01-01T00:00:00.000Z",148 updatedAt: "2026-01-01T00:00:00.000Z",
@@ -166,14 +156,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa · 7 unmodified lines
166 getThreadShellById: () => Effect.die("unused"),156 getThreadShellById: () => Effect.die("unused"),
167 getThreadDetailById: () => Effect.die("unused"),157 getThreadDetailById: () => Effect.die("unused"),
168 }),158 }),
169 Effect.provideService(OrchestrationEngineService, {159 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
170 readEvents: () => Stream.empty,160 readEvents: () => Stream.empty,
171 dispatch: (command) =>161 dispatch: (command) =>
172 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(162 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
173 Effect.as({ sequence: 1 }),163 Effect.as({ sequence: 1 }),
174 ),164 ),
175 streamDomainEvents: Stream.empty,165 streamDomainEvents: Stream.empty,
176 } satisfies OrchestrationEngineShape),166 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
177 Effect.provide(NodeServices.layer),167 Effect.provide(NodeServices.layer),
178 );168 );
179 169
@@ -188,12 +178,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa · 8 unmodified lines
188it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () =>178it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () =>
189 Effect.gen(function* () {179 Effect.gen(function* () {
190 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);180 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
191 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(181 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
192 Effect.provideService(ServerConfig, {182 Effect.provideService(ServerConfig.ServerConfig, {
193 cwd: "/tmp/startup-project",183 cwd: "/tmp/startup-project",
194 autoBootstrapProjectFromCwd: true,184 autoBootstrapProjectFromCwd: true,
195 } as never),185 } as never),
196 Effect.provideService(ProjectionSnapshotQuery, {186 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
197 getCommandReadModel: () => Effect.die("unused"),187 getCommandReadModel: () => Effect.die("unused"),
198 getSnapshot: () => Effect.die("unused"),188 getSnapshot: () => Effect.die("unused"),
199 getShellSnapshot: () => Effect.die("unused"),189 getShellSnapshot: () => Effect.die("unused"),
@@ -208,14 +198,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when · 8 unmodified lines
208 getThreadShellById: () => Effect.die("unused"),198 getThreadShellById: () => Effect.die("unused"),
209 getThreadDetailById: () => Effect.die("unused"),199 getThreadDetailById: () => Effect.die("unused"),
210 }),200 }),
211 Effect.provideService(OrchestrationEngineService, {201 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
212 readEvents: () => Stream.empty,202 readEvents: () => Stream.empty,
213 dispatch: (command) =>203 dispatch: (command) =>
214 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(204 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
215 Effect.as({ sequence: 1 }),205 Effect.as({ sequence: 1 }),
216 ),206 ),
217 streamDomainEvents: Stream.empty,207 streamDomainEvents: Stream.empty,
218 } satisfies OrchestrationEngineShape),208 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
219 Effect.provide(NodeServices.layer),209 Effect.provide(NodeServices.layer),
220 );210 );
221 211
@@ -236,12 +226,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa · 14 unmodified lines
236 });226 });
237 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);227 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
238 228
239 const error = yield* resolveAutoBootstrapWelcomeTargets.pipe(229 const error = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
240 Effect.provideService(ServerConfig, {230 Effect.provideService(ServerConfig.ServerConfig, {
241 cwd: "/tmp/startup-project",231 cwd: "/tmp/startup-project",
242 autoBootstrapProjectFromCwd: true,232 autoBootstrapProjectFromCwd: true,
243 } as never),233 } as never),
244 Effect.provideService(ProjectionSnapshotQuery, {234 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
245 getCommandReadModel: () => Effect.die("unused"),235 getCommandReadModel: () => Effect.die("unused"),
246 getSnapshot: () => Effect.die("unused"),236 getSnapshot: () => Effect.die("unused"),
247 getShellSnapshot: () => Effect.die("unused"),237 getShellSnapshot: () => Effect.die("unused"),
@@ -256,14 +246,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa · 8 unmodified lines
256 getThreadShellById: () => Effect.die("unused"),246 getThreadShellById: () => Effect.die("unused"),
257 getThreadDetailById: () => Effect.die("unused"),247 getThreadDetailById: () => Effect.die("unused"),
258 }),248 }),
259 Effect.provideService(OrchestrationEngineService, {249 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
260 readEvents: () => Stream.empty,250 readEvents: () => Stream.empty,
261 dispatch: (command) =>251 dispatch: (command) =>
262 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(252 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
263 Effect.as({ sequence: 1 }),253 Effect.as({ sequence: 1 }),
264 ),254 ),
265 streamDomainEvents: Stream.empty,255 streamDomainEvents: Stream.empty,
266 } satisfies OrchestrationEngineShape),256 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
267 Effect.provideService(Crypto.Crypto, {257 Effect.provideService(Crypto.Crypto, {
268 ...crypto,258 ...crypto,
269 randomUUIDv4: Effect.fail(uuidError),259 randomUUIDv4: Effect.fail(uuidError),
apps/server/src/serverRuntimeStartup.ts+53−48
@@ -7,8 +7,10 @@ import { · 6 unmodified lines
7 ProviderInstanceId,7 ProviderInstanceId,
8 ThreadId,8 ThreadId,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import * as Data from "effect/Data";10import * as Console from "effect/Console";
11import * as Context from "effect/Context";
11import * as Crypto from "effect/Crypto";12import * as Crypto from "effect/Crypto";
13import * as DateTime from "effect/DateTime";
12import * as Deferred from "effect/Deferred";14import * as Deferred from "effect/Deferred";
13import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
14import * as Exit from "effect/Exit";16import * as Exit from "effect/Exit";
@@ -17,23 +19,21 @@ import * as Option from "effect/Option"; · 2 unmodified lines
17import * as Path from "effect/Path";19import * as Path from "effect/Path";
18import * as Queue from "effect/Queue";20import * as Queue from "effect/Queue";
19import * as Ref from "effect/Ref";21import * as Ref from "effect/Ref";
22import * as Schema from "effect/Schema";
20import * as Scope from "effect/Scope";23import * as Scope from "effect/Scope";
21import * as Context from "effect/Context";
22import * as Console from "effect/Console";
23import * as DateTime from "effect/DateTime";
24 24
25import { ServerConfig } from "./config.ts";25import * as ServerConfig from "./config.ts";
26import { Keybindings } from "./keybindings.ts";26import * as Keybindings from "./keybindings.ts";
27import * as ExternalLauncher from "./process/externalLauncher.ts";27import * as ExternalLauncher from "./process/externalLauncher.ts";
28import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";28import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
29import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";29import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
30import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts";30import * as OrchestrationReactor from "./orchestration/Services/OrchestrationReactor.ts";
31import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";31import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
32import { ServerSettingsService } from "./serverSettings.ts";32import * as ServerSettings from "./serverSettings.ts";
33import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";33import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
34import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";34import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
35import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";35import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
36import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts";36import * as ProviderSessionReaper from "./provider/Services/ProviderSessionReaper.ts";
37import {37import {
38 formatHeadlessServeOutput,38 formatHeadlessServeOutput,
39 formatHostForUrl,39 formatHostForUrl,
@@ -41,22 +41,30 @@ import { · 1 unmodified line
41 issueHeadlessServeAccessInfo,41 issueHeadlessServeAccessInfo,
42} from "./startupAccess.ts";42} from "./startupAccess.ts";
43 43
44export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{44export class ServerRuntimeStartupError extends Schema.TaggedErrorClass<ServerRuntimeStartupError>()(
45 readonly message: string;45 "ServerRuntimeStartupError",
46 readonly cause?: unknown;46 {
47}> {}47 stage: Schema.Literal("command-readiness"),
48 48 cause: Schema.optional(Schema.Defect()),
49export interface ServerRuntimeStartupShape {49 },
50 readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>;50) {
51 readonly markHttpListening: Effect.Effect<void>;51 override get message(): string {
52 readonly enqueueCommand: <A, E>(52 switch (this.stage) {
53 effect: Effect.Effect<A, E>,53 case "command-readiness":
54 ) => Effect.Effect<A, E | ServerRuntimeStartupError>;54 return "Server runtime startup failed before command readiness.";
55 }
56 }
55}57}
56 58
57export class ServerRuntimeStartup extends Context.Service<59export class ServerRuntimeStartup extends Context.Service<
58 ServerRuntimeStartup,60 ServerRuntimeStartup,
59 ServerRuntimeStartupShape61 {
62 readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>;
63 readonly markHttpListening: Effect.Effect<void>;
64 readonly enqueueCommand: <A, E>(
65 effect: Effect.Effect<A, E>,
66 ) => Effect.Effect<A, E | ServerRuntimeStartupError>;
67 }
60>()("t3/serverRuntimeStartup") {}68>()("t3/serverRuntimeStartup") {}
61 69
62interface QueuedCommand {70interface QueuedCommand {
@@ -124,8 +132,8 @@ export const makeCommandGate = Effect.gen(function* () { · 61 unmodified lines
124});132});
125 133
126export const recordStartupHeartbeat = Effect.gen(function* () {134export const recordStartupHeartbeat = Effect.gen(function* () {
127 const analytics = yield* AnalyticsService;135 const analytics = yield* AnalyticsService.AnalyticsService;
128 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;136 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
129 137
130 const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe(138 const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe(
131 Effect.catch((cause) =>139 Effect.catch((cause) =>
@@ -160,7 +168,7 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ · 28 unmodified lines
160});168});
161 169
162export const resolveWelcomeBase = Effect.gen(function* () {170export const resolveWelcomeBase = Effect.gen(function* () {
163 const serverConfig = yield* ServerConfig;171 const serverConfig = yield* ServerConfig.ServerConfig;
164 const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean);172 const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean);
165 const projectName = segments[segments.length - 1] ?? "project";173 const projectName = segments[segments.length - 1] ?? "project";
166 174
@@ -173,9 +181,9 @@ export const resolveWelcomeBase = Effect.gen(function* () { · 6 unmodified lines
173export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {181export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
174 const crypto = yield* Crypto.Crypto;182 const crypto = yield* Crypto.Crypto;
175 const randomUUID = crypto.randomUUIDv4;183 const randomUUID = crypto.randomUUIDv4;
176 const serverConfig = yield* ServerConfig;184 const serverConfig = yield* ServerConfig.ServerConfig;
177 const projectionReadModelQuery = yield* ProjectionSnapshotQuery;185 const projectionReadModelQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
178 const orchestrationEngine = yield* OrchestrationEngineService;186 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
179 const path = yield* Path.Path;187 const path = yield* Path.Path;
180 188
181 let bootstrapProjectId: ProjectId | undefined;189 let bootstrapProjectId: ProjectId | undefined;
@@ -243,7 +251,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { · 61 unmodified lines
243});251});
244 252
245const resolveStartupBrowserTarget = Effect.gen(function* () {253const resolveStartupBrowserTarget = Effect.gen(function* () {
246 const serverConfig = yield* ServerConfig;254 const serverConfig = yield* ServerConfig.ServerConfig;
247 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;255 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
248 const localUrl = `http://localhost:${serverConfig.port}`;256 const localUrl = `http://localhost:${serverConfig.port}`;
249 const bindUrl =257 const bindUrl =
@@ -260,7 +268,7 @@ const resolveStartupBrowserTarget = Effect.gen(function* () { · 10 unmodified lines
260 268
261const maybeOpenBrowser = (target: string) =>269const maybeOpenBrowser = (target: string) =>
262 Effect.gen(function* () {270 Effect.gen(function* () {
263 const serverConfig = yield* ServerConfig;271 const serverConfig = yield* ServerConfig.ServerConfig;
264 if (serverConfig.noBrowser) {272 if (serverConfig.noBrowser) {
265 return;273 return;
266 }274 }
@@ -281,14 +289,14 @@ const runStartupPhase = <A, E, R>(phase: string, effect: Effect.Effect<A, E, R>) · 14 unmodified lines
281 Effect.withSpan(`server.startup.${phase}`),289 Effect.withSpan(`server.startup.${phase}`),
282 );290 );
283 291
284export const makeServerRuntimeStartup = Effect.gen(function* () {292const make = Effect.gen(function* () {
285 const serverConfig = yield* ServerConfig;293 const serverConfig = yield* ServerConfig.ServerConfig;
286 const keybindings = yield* Keybindings;294 const keybindings = yield* Keybindings.Keybindings;
287 const orchestrationReactor = yield* OrchestrationReactor;295 const orchestrationReactor = yield* OrchestrationReactor.OrchestrationReactor;
288 const providerSessionReaper = yield* ProviderSessionReaper;296 const providerSessionReaper = yield* ProviderSessionReaper.ProviderSessionReaper;
289 const lifecycleEvents = yield* ServerLifecycleEvents;297 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
290 const serverSettings = yield* ServerSettingsService;298 const serverSettings = yield* ServerSettings.ServerSettingsService;
291 const serverEnvironment = yield* ServerEnvironment;299 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
292 const crypto = yield* Crypto.Crypto;300 const crypto = yield* Crypto.Crypto;
293 301
294 const commandGate = yield* makeCommandGate;302 const commandGate = yield* makeCommandGate;
@@ -409,7 +417,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () { · 114 unmodified lines
409 const startupExit = yield* Effect.exit(startup);417 const startupExit = yield* Effect.exit(startup);
410 if (Exit.isFailure(startupExit)) {418 if (Exit.isFailure(startupExit)) {
411 const error = new ServerRuntimeStartupError({419 const error = new ServerRuntimeStartupError({
412 message: "Server runtime startup failed before command readiness.",420 stage: "command-readiness",
413 cause: startupExit.cause,421 cause: startupExit.cause,
414 });422 });
415 yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause });423 yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause });
@@ -461,10 +469,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () { · 45 unmodified lines
461 awaitCommandReady: commandGate.awaitCommandReady,469 awaitCommandReady: commandGate.awaitCommandReady,
462 markHttpListening: Deferred.succeed(httpListening, undefined),470 markHttpListening: Deferred.succeed(httpListening, undefined),
463 enqueueCommand: commandGate.enqueueCommand,471 enqueueCommand: commandGate.enqueueCommand,
464 } satisfies ServerRuntimeStartupShape;472 } satisfies ServerRuntimeStartup["Service"];
465});473});
466 474
467export const ServerRuntimeStartupLive = Layer.effect(475export const layer = Layer.effect(ServerRuntimeStartup, make);
468 ServerRuntimeStartup,
469 makeServerRuntimeStartup,
470);
apps/server/src/serverRuntimeState.ts+3−3
@@ -5,7 +5,7 @@ import * as Option from "effect/Option"; · 4 unmodified lines
5import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
6 6
7import { writeFileStringAtomically } from "./atomicWrite.ts";7import { writeFileStringAtomically } from "./atomicWrite.ts";
8import { type ServerConfigShape } from "./config.ts";8import type * as ServerConfig from "./config.ts";
9import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";9import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
10 10
11export const PersistedServerRuntimeState = Schema.Struct({11export const PersistedServerRuntimeState = Schema.Struct({
@@ -23,7 +23,7 @@ const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect( · 11 unmodified lines
23);23);
24 24
25const runtimeOriginForConfig = (25const runtimeOriginForConfig = (
26 config: Pick<ServerConfigShape, "host">,26 config: Pick<ServerConfig.ServerConfig["Service"], "host">,
27 port: number,27 port: number,
28): PersistedServerRuntimeState["origin"] => {28): PersistedServerRuntimeState["origin"] => {
29 const hostname =29 const hostname =
@@ -32,7 +32,7 @@ const runtimeOriginForConfig = ( · 2 unmodified lines
32};32};
33 33
34export const makePersistedServerRuntimeState = (input: {34export const makePersistedServerRuntimeState = (input: {
35 readonly config: Pick<ServerConfigShape, "host">;35 readonly config: Pick<ServerConfig.ServerConfig["Service"], "host">;
36 readonly port: number;36 readonly port: number;
37}): Effect.Effect<PersistedServerRuntimeState> =>37}): Effect.Effect<PersistedServerRuntimeState> =>
38 Effect.map(DateTime.now, (now) => ({38 Effect.map(DateTime.now, (now) => ({
apps/server/src/serverSettings.test.ts+19−17
@@ -13,14 +13,16 @@ import * as Duration from "effect/Duration"; · 12 unmodified lines
13import * as FileSystem from "effect/FileSystem";13import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
16import { ServerConfig } from "./config.ts";16import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
17import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";17import * as ServerConfig from "./config.ts";
18import * as ServerSettingsModule from "./serverSettings.ts";
18 19
19const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);20const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);
20const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);21const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);
21 22
22const makeServerSettingsLayer = () =>23const makeServerSettingsLayer = () =>
23 ServerSettingsLive.pipe(24 ServerSettingsModule.layer.pipe(
25 Layer.provide(ServerSecretStore.layer),
24 Layer.provideMerge(26 Layer.provideMerge(
25 Layer.fresh(27 Layer.fresh(
26 ServerConfig.layerTest(process.cwd(), {28 ServerConfig.layerTest(process.cwd(), {
@@ -77,7 +79,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 50 unmodified lines
77 79
78 it.effect("deep merges nested settings updates without dropping siblings", () =>80 it.effect("deep merges nested settings updates without dropping siblings", () =>
79 Effect.gen(function* () {81 Effect.gen(function* () {
80 const serverSettings = yield* ServerSettingsService;82 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
81 83
82 yield* serverSettings.updateSettings({84 yield* serverSettings.updateSettings({
83 providers: {85 providers: {
@@ -145,7 +147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 61 unmodified lines
145 147
146 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>148 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
147 Effect.gen(function* () {149 Effect.gen(function* () {
148 const serverSettings = yield* ServerSettingsService;150 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
149 151
150 // Start with Claude text generation selection152 // Start with Claude text generation selection
151 yield* serverSettings.updateSettings({153 yield* serverSettings.updateSettings({
@@ -183,7 +185,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 31 unmodified lines
183 185
184 it.effect("preserves custom provider instance text generation selections", () =>186 it.effect("preserves custom provider instance text generation selections", () =>
185 Effect.gen(function* () {187 Effect.gen(function* () {
186 const serverSettings = yield* ServerSettingsService;188 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
187 189
188 const next = yield* serverSettings.updateSettings({190 const next = yield* serverSettings.updateSettings({
189 providerInstances: {191 providerInstances: {
@@ -210,7 +212,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 20 unmodified lines
210 "uses explicit provider instance enabled state over legacy provider enabled state",212 "uses explicit provider instance enabled state over legacy provider enabled state",
211 () =>213 () =>
212 Effect.gen(function* () {214 Effect.gen(function* () {
213 const serverSettings = yield* ServerSettingsService;215 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
214 const instanceId = ProviderInstanceId.make("claude_openrouter");216 const instanceId = ProviderInstanceId.make("claude_openrouter");
215 217
216 const next = yield* serverSettings.updateSettings({218 const next = yield* serverSettings.updateSettings({
@@ -241,7 +243,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 24 unmodified lines
241 243
242 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>244 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>
243 Effect.gen(function* () {245 Effect.gen(function* () {
244 const serverSettings = yield* ServerSettingsService;246 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
245 const instanceId = ProviderInstanceId.make("openrouter_text");247 const instanceId = ProviderInstanceId.make("openrouter_text");
246 248
247 const next = yield* serverSettings.updateSettings({249 const next = yield* serverSettings.updateSettings({
@@ -267,7 +269,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 19 unmodified lines
267 269
268 it.effect("drops stale text generation options when resetting model selection", () =>270 it.effect("drops stale text generation options when resetting model selection", () =>
269 Effect.gen(function* () {271 Effect.gen(function* () {
270 const serverSettings = yield* ServerSettingsService;272 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
271 273
272 yield* serverSettings.updateSettings({274 yield* serverSettings.updateSettings({
273 textGenerationModelSelection: {275 textGenerationModelSelection: {
@@ -300,7 +302,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 26 unmodified lines
300 302
301 it.effect("replaces provider instance maps when clearing optional fields", () =>303 it.effect("replaces provider instance maps when clearing optional fields", () =>
302 Effect.gen(function* () {304 Effect.gen(function* () {
303 const serverSettings = yield* ServerSettingsService;305 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
304 const codexId = ProviderInstanceId.make("codex");306 const codexId = ProviderInstanceId.make("codex");
305 307
306 yield* serverSettings.updateSettings({308 yield* serverSettings.updateSettings({
@@ -337,7 +339,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 30 unmodified lines
337 339
338 it.effect("trims provider path settings when updates are applied", () =>340 it.effect("trims provider path settings when updates are applied", () =>
339 Effect.gen(function* () {341 Effect.gen(function* () {
340 const serverSettings = yield* ServerSettingsService;342 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
341 343
342 const next = yield* serverSettings.updateSettings({344 const next = yield* serverSettings.updateSettings({
343 providers: {345 providers: {
@@ -382,7 +384,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 38 unmodified lines
382 384
383 it.effect("trims observability settings when updates are applied", () =>385 it.effect("trims observability settings when updates are applied", () =>
384 Effect.gen(function* () {386 Effect.gen(function* () {
385 const serverSettings = yield* ServerSettingsService;387 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
386 388
387 const next = yield* serverSettings.updateSettings({389 const next = yield* serverSettings.updateSettings({
388 addProjectBaseDirectory: " ~/Development ",390 addProjectBaseDirectory: " ~/Development ",
@@ -402,7 +404,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 13 unmodified lines
402 404
403 it.effect("defaults blank binary paths to provider executables", () =>405 it.effect("defaults blank binary paths to provider executables", () =>
404 Effect.gen(function* () {406 Effect.gen(function* () {
405 const serverSettings = yield* ServerSettingsService;407 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
406 408
407 const next = yield* serverSettings.updateSettings({409 const next = yield* serverSettings.updateSettings({
408 providers: {410 providers: {
@@ -422,8 +424,8 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 13 unmodified lines
422 424
423 it.effect("writes only non-default server settings to disk", () =>425 it.effect("writes only non-default server settings to disk", () =>
424 Effect.gen(function* () {426 Effect.gen(function* () {
425 const serverSettings = yield* ServerSettingsService;427 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
426 const serverConfig = yield* ServerConfig;428 const serverConfig = yield* ServerConfig.ServerConfig;
427 const fileSystem = yield* FileSystem.FileSystem;429 const fileSystem = yield* FileSystem.FileSystem;
428 const next = yield* serverSettings.updateSettings({430 const next = yield* serverSettings.updateSettings({
429 addProjectBaseDirectory: "~/Development",431 addProjectBaseDirectory: "~/Development",
@@ -469,8 +471,8 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 39 unmodified lines
469 471
470 it.effect("stores sensitive provider instance environment values outside settings.json", () =>472 it.effect("stores sensitive provider instance environment values outside settings.json", () =>
471 Effect.gen(function* () {473 Effect.gen(function* () {
472 const serverSettings = yield* ServerSettingsService;474 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
473 const serverConfig = yield* ServerConfig;475 const serverConfig = yield* ServerConfig.ServerConfig;
474 const fileSystem = yield* FileSystem.FileSystem;476 const fileSystem = yield* FileSystem.FileSystem;
475 const instanceId = ProviderInstanceId.make("codex_personal");477 const instanceId = ProviderInstanceId.make("codex_personal");
476 478
apps/server/src/serverSettings.ts+56−57
@@ -26,26 +26,26 @@ import { · 25 unmodified lines
26 type ServerSettingsPatch,26 type ServerSettingsPatch,
27} from "@t3tools/contracts";27} from "@t3tools/contracts";
28import * as Cache from "effect/Cache";28import * as Cache from "effect/Cache";
29import * as Cause from "effect/Cause";
30import * as Context from "effect/Context";
29import * as Deferred from "effect/Deferred";31import * as Deferred from "effect/Deferred";
30import * as Duration from "effect/Duration";32import * as Duration from "effect/Duration";
33import * as Equal from "effect/Equal";
31import * as Effect from "effect/Effect";34import * as Effect from "effect/Effect";
32import * as Exit from "effect/Exit";35import * as Exit from "effect/Exit";
33import * as FileSystem from "effect/FileSystem";36import * as FileSystem from "effect/FileSystem";
34import * as Layer from "effect/Layer";37import * as Layer from "effect/Layer";
35import * as Option from "effect/Option";38import * as Option from "effect/Option";
36import * as Path from "effect/Path";39import * as Path from "effect/Path";
37import * as Equal from "effect/Equal";
38import * as PubSub from "effect/PubSub";40import * as PubSub from "effect/PubSub";
39import * as Ref from "effect/Ref";41import * as Ref from "effect/Ref";
40import * as Schema from "effect/Schema";42import * as Schema from "effect/Schema";
41import * as SchemaIssue from "effect/SchemaIssue";43import * as SchemaIssue from "effect/SchemaIssue";
44import * as Semaphore from "effect/Semaphore";
42import * as Scope from "effect/Scope";45import * as Scope from "effect/Scope";
43import * as Context from "effect/Context";
44import * as Stream from "effect/Stream";46import * as Stream from "effect/Stream";
45import * as Cause from "effect/Cause";
46import * as Semaphore from "effect/Semaphore";
47import { writeFileStringAtomically } from "./atomicWrite.ts";47import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { ServerConfig } from "./config.ts";48import * as ServerConfig from "./config.ts";
49import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";49import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
50import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";50import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
51import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";51import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
@@ -109,59 +109,60 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS · 57 unmodified lines
109 return { ...settings, providerInstances };109 return { ...settings, providerInstances };
110}110}
111 111
112export interface ServerSettingsShape {112export class ServerSettingsService extends Context.Service<
113 /** Start the settings runtime and attach file watching. */113 ServerSettingsService,
114 readonly start: Effect.Effect<void, ServerSettingsError>;114 {
115 115 /** Start the settings runtime and attach file watching. */
116 /** Await settings runtime readiness. */116 readonly start: Effect.Effect<void, ServerSettingsError>;
117 readonly ready: Effect.Effect<void, ServerSettingsError>;
118 117
119 /** Read the current settings. */118 /** Await settings runtime readiness. */
120 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;119 readonly ready: Effect.Effect<void, ServerSettingsError>;
121 120
122 /** Patch settings and persist. Returns the new full settings object. */121 /** Read the current settings. */
123 readonly updateSettings: (122 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;
124 patch: ServerSettingsPatch,
125 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
126 123
127 /** Stream of settings change events. */124 /** Patch settings and persist. Returns the new full settings object. */
128 readonly streamChanges: Stream.Stream<ServerSettings>;125 readonly updateSettings: (
129}126 patch: ServerSettingsPatch,
127 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
130 128
131export class ServerSettingsService extends Context.Service<129 /** Stream of settings change events. */
132 ServerSettingsService,130 readonly streamChanges: Stream.Stream<ServerSettings>;
133 ServerSettingsShape131 }
134>()("t3/serverSettings/ServerSettingsService") {132>()("t3/serverSettings/ServerSettingsService") {
135 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>133 /** @deprecated Import and use `layerTest` from this module. */
136 Layer.effect(134 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides);
137 ServerSettingsService,
138 Effect.gen(function* () {
139 const { automaticGitFetchInterval, ...overridesForMerge } = overrides;
140 const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);
141 const initialSettings = yield* normalizeServerSettings({
142 ...merged,
143 ...(automaticGitFetchInterval !== undefined
144 ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }
145 : {}),
146 });
147 const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);
148
149 return {
150 start: Effect.void,
151 ready: Effect.void,
152 getSettings: Ref.get(currentSettingsRef),
153 updateSettings: (patch) =>
154 Ref.get(currentSettingsRef).pipe(
155 Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),
156 Effect.flatMap(normalizeServerSettings),
157 Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),
158 ),
159 streamChanges: Stream.empty,
160 } satisfies ServerSettingsShape;
161 }),
162 );
163}135}
164 136
137const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>
138 Effect.gen(function* () {
139 const { automaticGitFetchInterval, ...overridesForMerge } = overrides;
140 const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);
141 const initialSettings = yield* normalizeServerSettings({
142 ...merged,
143 ...(automaticGitFetchInterval !== undefined
144 ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }
145 : {}),
146 });
147 const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);
148
149 return {
150 start: Effect.void,
151 ready: Effect.void,
152 getSettings: Ref.get(currentSettingsRef),
153 updateSettings: (patch) =>
154 Ref.get(currentSettingsRef).pipe(
155 Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),
156 Effect.flatMap(normalizeServerSettings),
157 Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),
158 ),
159 streamChanges: Stream.empty,
160 } satisfies ServerSettingsService["Service"];
161 });
162
163export const layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>
164 Layer.effect(ServerSettingsService, makeTest(overrides));
165
165const ServerSettingsJson = fromLenientJson(ServerSettings);166const ServerSettingsJson = fromLenientJson(ServerSettings);
166const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);167const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
167 168
@@ -255,8 +256,8 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow · 87 unmodified lines
255 return Object.is(current, defaults) ? undefined : current;256 return Object.is(current, defaults) ? undefined : current;
256}257}
257 258
258const makeServerSettings = Effect.gen(function* () {259const make = Effect.gen(function* () {
259 const { settingsPath } = yield* ServerConfig;260 const { settingsPath } = yield* ServerConfig.ServerConfig;
260 const fs = yield* FileSystem.FileSystem;261 const fs = yield* FileSystem.FileSystem;
261 const pathService = yield* Path.Path;262 const pathService = yield* Path.Path;
262 const secretStore = yield* ServerSecretStore.ServerSecretStore;263 const secretStore = yield* ServerSecretStore.ServerSecretStore;
@@ -578,9 +579,7 @@ const makeServerSettings = Effect.gen(function* () { · 315 unmodified lines
578 Stream.map(resolveTextGenerationProvider),579 Stream.map(resolveTextGenerationProvider),
579 );580 );
580 },581 },
581 } satisfies ServerSettingsShape;582 } satisfies ServerSettingsService["Service"];
582});583});
583 584
584export const ServerSettingsLive = Layer.effect(ServerSettingsService, makeServerSettings).pipe(585export const layer = Layer.effect(ServerSettingsService, make);
585 Layer.provide(ServerSecretStore.layer),
586);
apps/server/src/vcs/GitVcsDriverCore.test.ts+73−0
@@ -100,6 +100,41 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { · 99 unmodified lines
100 assert.deepStrictEqual(paths, ["complete.txt", "final.txt"]);100 assert.deepStrictEqual(paths, ["complete.txt", "final.txt"]);
101 }),101 }),
102 );102 );
103
104 it.effect("honors whitespace filtering for worktree and branch previews", () =>
105 Effect.gen(function* () {
106 const cwd = yield* makeTmpDir();
107 const { initialBranch } = yield* initRepoWithCommit(cwd);
108 const driver = yield* GitVcsDriver.GitVcsDriver;
109 yield* git(cwd, ["checkout", "-b", "feature/whitespace"]);
110 yield* writeTextFile(cwd, "README.md", "# test\n");
111 yield* git(cwd, ["add", "README.md"]);
112 yield* git(cwd, ["commit", "-m", "change whitespace"]);
113 yield* writeTextFile(cwd, "README.md", "# test\n");
114
115 const included = yield* driver.getReviewDiffPreview({
116 cwd,
117 baseRef: initialBranch,
118 ignoreWhitespace: false,
119 });
120 const ignored = yield* driver.getReviewDiffPreview({
121 cwd,
122 baseRef: initialBranch,
123 ignoreWhitespace: true,
124 });
125
126 assert.isNotEmpty(included.sources.find((source) => source.kind === "working-tree")?.diff);
127 assert.isNotEmpty(included.sources.find((source) => source.kind === "branch-range")?.diff);
128 assert.strictEqual(
129 ignored.sources.find((source) => source.kind === "working-tree")?.diff,
130 "",
131 );
132 assert.strictEqual(
133 ignored.sources.find((source) => source.kind === "branch-range")?.diff,
134 "",
135 );
136 }),
137 );
103 });138 });
104 139
105 describe("repository status", () => {140 describe("repository status", () => {
@@ -342,6 +377,44 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { · 236 unmodified lines
342 });377 });
343 378
344 describe("refName operations", () => {379 describe("refName operations", () => {
380 it.effect("optionally includes remote refs that match local branches", () =>
381 Effect.gen(function* () {
382 const cwd = yield* makeTmpDir();
383 const remote = yield* makeTmpDir("git-vcs-driver-remote-");
384 const { initialBranch } = yield* initRepoWithCommit(cwd);
385 yield* git(remote, ["init", "--bare"]);
386 yield* git(cwd, ["remote", "add", "origin", remote]);
387 yield* git(cwd, ["push", "-u", "origin", initialBranch]);
388 const driver = yield* GitVcsDriver.GitVcsDriver;
389
390 const deduplicated = yield* driver.listRefs({ cwd });
391 assert.equal(
392 deduplicated.refs.some((ref) => ref.name === `origin/${initialBranch}`),
393 false,
394 );
395
396 const complete = yield* driver.listRefs({ cwd, includeMatchingRemoteRefs: true });
397 assert.equal(
398 complete.refs.some((ref) => ref.name === initialBranch),
399 true,
400 );
401 assert.equal(
402 complete.refs.some((ref) => ref.name === `origin/${initialBranch}`),
403 true,
404 );
405
406 const remoteOnly = yield* driver.listRefs({
407 cwd,
408 includeMatchingRemoteRefs: true,
409 refKind: "remote",
410 limit: 1,
411 });
412 assert.equal(remoteOnly.refs.length, 1);
413 assert.equal(remoteOnly.refs[0]?.name, `origin/${initialBranch}`);
414 assert.equal(remoteOnly.refs[0]?.isRemote, true);
415 }),
416 );
417
345 it.effect("creates, checks out, renames, and lists refs", () =>418 it.effect("creates, checks out, renames, and lists refs", () =>
346 Effect.gen(function* () {419 Effect.gen(function* () {
347 const cwd = yield* makeTmpDir();420 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+25−6
@@ -1817,7 +1817,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1816 unmodified lines
1817 const dirtyTrackedResult = yield* executeGit(1817 const dirtyTrackedResult = yield* executeGit(
1818 "GitVcsDriver.getReviewDiffPreview.dirtyTracked",1818 "GitVcsDriver.getReviewDiffPreview.dirtyTracked",
1819 input.cwd,1819 input.cwd,
1820 ["diff", "--patch", "--minimal", "HEAD", "--"],1820 [
1821 "diff",
1822 "--patch",
1823 "--minimal",
1824 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
1825 "HEAD",
1826 "--",
1827 ],
1821 {1828 {
1822 maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,1829 maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
1823 appendTruncationMarker: true,1830 appendTruncationMarker: true,
@@ -1843,7 +1850,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 19 unmodified lines
1843 ? yield* executeGit(1850 ? yield* executeGit(
1844 "GitVcsDriver.getReviewDiffPreview.base",1851 "GitVcsDriver.getReviewDiffPreview.base",
1845 input.cwd,1852 input.cwd,
1846 ["diff", "--patch", "--minimal", `${baseRef}...HEAD`],1853 [
1854 "diff",
1855 "--patch",
1856 "--minimal",
1857 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
1858 `${baseRef}...HEAD`,
1859 ],
1847 {1860 {
1848 maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,1861 maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
1849 appendTruncationMarker: true,1862 appendTruncationMarker: true,
@@ -2127,11 +2140,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 277 unmodified lines
2127 })2140 })
2128 : [];2141 : [];
2129 2142
2143 const allBranches = input.includeMatchingRemoteRefs
2144 ? [...localBranches, ...remoteBranches]
2145 : dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]);
2146 const branchesForKind =
2147 input.refKind === "local"
2148 ? allBranches.filter((ref) => !ref.isRemote)
2149 : input.refKind === "remote"
2150 ? allBranches.filter((ref) => ref.isRemote)
2151 : allBranches;
2130 const refs = paginateBranches({2152 const refs = paginateBranches({
2131 refs: filterBranchesForListQuery(2153 refs: filterBranchesForListQuery(branchesForKind, input.query),
2132 dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),
2133 input.query,
2134 ),
2135 cursor: input.cursor,2154 cursor: input.cursor,
2136 limit: input.limit,2155 limit: input.limit,
2137 });2156 });
apps/server/src/ws.ts+2−2
@@ -1489,7 +1489,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => · 1488 unmodified lines
1489 [WS_METHODS.previewAutomationConnect]: (input) =>1489 [WS_METHODS.previewAutomationConnect]: (input) =>
1490 observeRpcStreamEffect(1490 observeRpcStreamEffect(
1491 WS_METHODS.previewAutomationConnect,1491 WS_METHODS.previewAutomationConnect,
1492 previewAutomationBroker.connect(input.clientId),1492 previewAutomationBroker.connect(input),
1493 { "rpc.aggregate": "preview-automation" },1493 { "rpc.aggregate": "preview-automation" },
1494 ),1494 ),
1495 [WS_METHODS.previewAutomationRespond]: (input) =>1495 [WS_METHODS.previewAutomationRespond]: (input) =>
@@ -1507,7 +1507,7 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => · 11 unmodified lines
1507 [WS_METHODS.previewAutomationClearOwner]: (input) =>1507 [WS_METHODS.previewAutomationClearOwner]: (input) =>
1508 observeRpcEffect(1508 observeRpcEffect(
1509 WS_METHODS.previewAutomationClearOwner,1509 WS_METHODS.previewAutomationClearOwner,
1510 previewAutomationBroker.clearOwner(input.clientId),1510 previewAutomationBroker.clearOwner(input),
1511 { "rpc.aggregate": "preview-automation" },1511 { "rpc.aggregate": "preview-automation" },
1512 ),1512 ),
1513 [WS_METHODS.subscribePreviewEvents]: (_input) =>1513 [WS_METHODS.subscribePreviewEvents]: (_input) =>
apps/web/src/components/ChatView.tsx+13−120
@@ -42,7 +42,7 @@ import { nextTerminalId, resolveTerminalSessionLabel } from "@t3tools/shared/ter · 41 unmodified lines
42import { Debouncer } from "@tanstack/react-pacer";42import { Debouncer } from "@tanstack/react-pacer";
43import { useAtomValue } from "@effect/atom-react";43import { useAtomValue } from "@effect/atom-react";
44import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";44import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
45import { useNavigate, useSearch } from "@tanstack/react-router";45import { useNavigate } from "@tanstack/react-router";
46import { useShallow } from "zustand/react/shallow";46import { useShallow } from "zustand/react/shallow";
47import {47import {
48 isAtomCommandInterrupted,48 isAtomCommandInterrupted,
@@ -55,7 +55,7 @@ import * as Cause from "effect/Cause"; · 6 unmodified lines
55import { AsyncResult } from "effect/unstable/reactivity";55import { AsyncResult } from "effect/unstable/reactivity";
56import { isElectron } from "../env";56import { isElectron } from "../env";
57import { readLocalApi } from "../localApi";57import { readLocalApi } from "../localApi";
58import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";58import { useDiffPanelStore } from "../diffPanelStore";
59import {59import {
60 collapseExpandedComposerCursor,60 collapseExpandedComposerCursor,
61 parseStandaloneComposerSlashCommand,61 parseStandaloneComposerSlashCommand,
@@ -104,7 +104,7 @@ import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; · 42 unmodified lines
104import { useMediaQuery } from "../hooks/useMediaQuery";104import { useMediaQuery } from "../hooks/useMediaQuery";
105import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";105import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";
106import {106import {
107 selectActiveRightPanelKindWithUrl,107 selectActiveRightPanel,
108 selectActiveRightPanelSurface,108 selectActiveRightPanelSurface,
109 selectThreadRightPanelState,109 selectThreadRightPanelState,
110 type RightPanelSurface,110 type RightPanelSurface,
@@ -1034,10 +1034,6 @@ function ChatViewContent(props: ChatViewProps) { · 923 unmodified lines
1034 const timestampFormat = settings.timestampFormat;1034 const timestampFormat = settings.timestampFormat;
1035 const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;1035 const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;
1036 const navigate = useNavigate();1036 const navigate = useNavigate();
1037 const rawSearch = useSearch({
1038 strict: false,
1039 select: (params) => parseDiffRouteSearch(params),
1040 });
1041 const { resolvedTheme } = useTheme();1037 const { resolvedTheme } = useTheme();
1042 // Granular store selectors — avoid subscribing to prompt changes.1038 // Granular store selectors — avoid subscribing to prompt changes.
1043 const composerRuntimeMode = useComposerDraftStore(1039 const composerRuntimeMode = useComposerDraftStore(
@@ -1217,7 +1213,6 @@ function ChatViewContent(props: ChatViewProps) { · 173 unmodified lines
1217 composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;1213 composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
1218 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;1214 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
1219 const canCheckoutPullRequestIntoThread = isLocalDraftThread;1215 const canCheckoutPullRequestIntoThread = isLocalDraftThread;
1220 const diffOpen = rawSearch.diff === "1";
1221 const activeThreadId = activeThread?.id ?? null;1216 const activeThreadId = activeThread?.id ?? null;
1222 const runningTerminalIds = useThreadRunningTerminalIds({1217 const runningTerminalIds = useThreadRunningTerminalIds({
1223 environmentId: activeThread?.environmentId ?? null,1218 environmentId: activeThread?.environmentId ?? null,
@@ -1259,8 +1254,9 @@ function ChatViewContent(props: ChatViewProps) { · 35 unmodified lines
1259 );1254 );
1260 const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;1255 const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;
1261 const activeRightPanelKind = useRightPanelStore((state) =>1256 const activeRightPanelKind = useRightPanelStore((state) =>
1262 selectActiveRightPanelKindWithUrl(state.byThreadKey, activeThreadRef, diffOpen),1257 selectActiveRightPanel(state.byThreadKey, activeThreadRef),
1263 );1258 );
1259 const diffOpen = activeRightPanelKind === "diff";
1264 const rightPanelState = useRightPanelStore((state) =>1260 const rightPanelState = useRightPanelStore((state) =>
1265 selectThreadRightPanelState(state.byThreadKey, activeThreadRef),1261 selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
1266 );1262 );
@@ -1295,11 +1291,6 @@ function ChatViewContent(props: ChatViewProps) { · 28 unmodified lines
1295 1291
1296 const planSidebarOpen = activeRightPanelKind === "plan";1292 const planSidebarOpen = activeRightPanelKind === "plan";
1297 1293
1298 useEffect(() => {
1299 if (!activeThreadRef || !diffOpen) return;
1300 useRightPanelStore.getState().open(activeThreadRef, "diff");
1301 }, [activeThreadRef, diffOpen]);
1302
1303 const existingOpenTerminalThreadKeys = useMemo(() => {1294 const existingOpenTerminalThreadKeys = useMemo(() => {
1304 const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);1295 const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
1305 return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));1296 return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));
@@ -2151,27 +2142,7 @@ function ChatViewContent(props: ChatViewProps) { · 845 unmodified lines
2151 if (activeThreadRef) {2142 if (activeThreadRef) {
2152 useRightPanelStore.getState().toggle(activeThreadRef, "diff");2143 useRightPanelStore.getState().toggle(activeThreadRef, "diff");
2153 }2144 }
2154 void navigate({2145 }, [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen]);
2155 to: "/$environmentId/$threadId",
2156 params: {
2157 environmentId,
2158 threadId,
2159 },
2160 replace: true,
2161 search: (previous) => {
2162 const rest = stripDiffSearchParams(previous);
2163 return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" };
2164 },
2165 });
2166 }, [
2167 activeThreadRef,
2168 diffOpen,
2169 environmentId,
2170 isServerThread,
2171 navigate,
2172 onDiffPanelOpen,
2173 threadId,
2174 ]);
2175 2146
2176 const envLocked = Boolean(2147 const envLocked = Boolean(
2177 activeThread &&2148 activeThread &&
@@ -2757,21 +2728,7 @@ function ChatViewContent(props: ChatViewProps) { · 579 unmodified lines
2757 if (!activeThreadRef || !isServerThread || !isGitRepo) return;2728 if (!activeThreadRef || !isServerThread || !isGitRepo) return;
2758 useRightPanelStore.getState().open(activeThreadRef, "diff");2729 useRightPanelStore.getState().open(activeThreadRef, "diff");
2759 onDiffPanelOpen?.();2730 onDiffPanelOpen?.();
2760 void navigate({2731 }, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);
2761 to: "/$environmentId/$threadId",
2762 params: { environmentId, threadId },
2763 replace: true,
2764 search: (previous) => ({ ...stripDiffSearchParams(previous), diff: "1" }),
2765 });
2766 }, [
2767 activeThreadRef,
2768 environmentId,
2769 isGitRepo,
2770 isServerThread,
2771 navigate,
2772 onDiffPanelOpen,
2773 threadId,
2774 ]);
2775 const addFilesSurface = useCallback(() => {2732 const addFilesSurface = useCallback(() => {
2776 if (!activeThreadRef || !activeProject) return;2733 if (!activeThreadRef || !activeProject) return;
2777 useRightPanelStore.getState().open(activeThreadRef, "files");2734 useRightPanelStore.getState().open(activeThreadRef, "files");
@@ -2789,30 +2746,13 @@ function ChatViewContent(props: ChatViewProps) { · 11 unmodified lines
2789 useRightPanelStore.getState().close(activeThreadRef);2746 useRightPanelStore.getState().close(activeThreadRef);
2790 return;2747 return;
2791 }2748 }
2792 if (diffOpen) {
2793 void navigate({
2794 to: "/$environmentId/$threadId",
2795 params: { environmentId, threadId },
2796 replace: true,
2797 search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
2798 });
2799 }
2800 const activeTabId = activePreviewState.activeTabId;2749 const activeTabId = activePreviewState.activeTabId;
2801 if (activeTabId) {2750 if (activeTabId) {
2802 useRightPanelStore.getState().openBrowser(activeThreadRef, activeTabId);2751 useRightPanelStore.getState().openBrowser(activeThreadRef, activeTabId);
2803 } else {2752 } else {
2804 createBrowserSurface();2753 createBrowserSurface();
2805 }2754 }
2806 }, [2755 }, [activePreviewState.activeTabId, activeThreadRef, createBrowserSurface, previewPanelOpen]);
2807 activePreviewState.activeTabId,
2808 activeThreadRef,
2809 createBrowserSurface,
2810 diffOpen,
2811 environmentId,
2812 navigate,
2813 previewPanelOpen,
2814 threadId,
2815 ]);
2816 const closePreviewPanel = useCallback(() => {2756 const closePreviewPanel = useCallback(() => {
2817 if (activeThreadRef) {2757 if (activeThreadRef) {
2818 setMaximizedRightPanelThreadKey(null);2758 setMaximizedRightPanelThreadKey(null);
@@ -2936,31 +2876,9 @@ function ChatViewContent(props: ChatViewProps) { · 117 unmodified lines
2936 }2876 }
2937 if (surface.kind === "diff" && !diffOpen) {2877 if (surface.kind === "diff" && !diffOpen) {
2938 onDiffPanelOpen?.();2878 onDiffPanelOpen?.();
2939 void navigate({
2940 to: "/$environmentId/$threadId",
2941 params: { environmentId, threadId },
2942 replace: true,
2943 search: (previous) => ({ ...stripDiffSearchParams(previous), diff: "1" }),
2944 });
2945 } else if (surface.kind !== "diff" && diffOpen) {
2946 void navigate({
2947 to: "/$environmentId/$threadId",
2948 params: { environmentId, threadId },
2949 replace: true,
2950 search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
2951 });
2952 }2879 }
2953 },2880 },
2954 [2881 [activeThreadRef, diffOpen, dismissPlanSidebarForCurrentTurn, onDiffPanelOpen, planSidebarOpen],
2955 activeThreadRef,
2956 diffOpen,
2957 dismissPlanSidebarForCurrentTurn,
2958 environmentId,
2959 navigate,
2960 onDiffPanelOpen,
2961 planSidebarOpen,
2962 threadId,
2963 ],
2964 );2882 );
2965 const toggleRightPanel = useCallback(() => {2883 const toggleRightPanel = useCallback(() => {
2966 if (!activeThreadRef) return;2884 if (!activeThreadRef) return;
@@ -3006,26 +2924,14 @@ function ChatViewContent(props: ChatViewProps) { · 39 unmodified lines
3006 }2924 }
3007 }2925 }
3008 }2926 }
3009 if (diffOpen && surfaces.some((surface) => surface.kind === "diff")) {
3010 void navigate({
3011 to: "/$environmentId/$threadId",
3012 params: { environmentId, threadId },
3013 replace: true,
3014 search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
3015 });
3016 }
3017 },2927 },
3018 [2928 [
3019 activeThreadRef,2929 activeThreadRef,
3020 activePreviewState.sessions,2930 activePreviewState.sessions,
3021 closePreview,2931 closePreview,
3022 closeTerminalMutation,2932 closeTerminalMutation,
3023 diffOpen,
3024 dismissPlanSidebarForCurrentTurn,2933 dismissPlanSidebarForCurrentTurn,
3025 environmentId,
3026 navigate,
3027 storeCloseTerminal,2934 storeCloseTerminal,
3028 threadId,
3029 ],2935 ],
3030 );2936 );
3031 const syncActivePreviewSurface = useCallback(() => {2937 const syncActivePreviewSurface = useCallback(() => {
@@ -4631,25 +4537,12 @@ function ChatViewContent(props: ChatViewProps) { · 1599 unmodified lines
4631 }, []);4537 }, []);
4632 const onOpenTurnDiff = useCallback(4538 const onOpenTurnDiff = useCallback(
4633 (turnId: TurnId, filePath?: string) => {4539 (turnId: TurnId, filePath?: string) => {
4634 if (!isServerThread) {4540 if (!isServerThread || !activeThreadRef) return;
4635 return;4541 useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);
4636 }4542 useRightPanelStore.getState().open(activeThreadRef, "diff");
4637 onDiffPanelOpen?.();4543 onDiffPanelOpen?.();
4638 void navigate({
4639 to: "/$environmentId/$threadId",
4640 params: {
4641 environmentId,
4642 threadId,
4643 },
4644 search: (previous) => {
4645 const rest = stripDiffSearchParams(previous);
4646 return filePath
4647 ? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
4648 : { ...rest, diff: "1", diffTurnId: turnId };
4649 },
4650 });
4651 },4544 },
4652 [environmentId, isServerThread, navigate, onDiffPanelOpen, threadId],4545 [activeThreadRef, isServerThread, onDiffPanelOpen],
4653 );4546 );
4654 // Both the Map and the revert handler are read from refs at call-time so4547 // Both the Map and the revert handler are read from refs at call-time so
4655 // the callback reference is fully stable and never busts context identity.4548 // the callback reference is fully stable and never busts context identity.
apps/web/src/components/DiffPanel.tsx+436−345
@@ -1,35 +1,28 @@
1import { useAtomValue } from "@effect/atom-react";1import { useAtomValue } from "@effect/atom-react";
2import { Virtualizer } from "@pierre/diffs/react";2import { useParams } from "@tanstack/react-router";
3import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
4import { scopeThreadRef } from "@t3tools/client-runtime/environment";
5import {3import {
6 isAtomCommandInterrupted,4 isAtomCommandInterrupted,
7 squashAtomCommandFailure,5 squashAtomCommandFailure,
8} from "@t3tools/client-runtime/state/runtime";6} from "@t3tools/client-runtime/state/runtime";
9import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";7import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";
10import {8import {
9 ArrowRightIcon,
10 CheckIcon,
11 ChevronDownIcon,11 ChevronDownIcon,
12 ChevronLeftIcon,
13 ChevronRightIcon,12 ChevronRightIcon,
14 Columns2Icon,13 Columns2Icon,
15 PilcrowIcon,14 PilcrowIcon,
16 Rows3Icon,15 Rows3Icon,
16 SearchIcon,
17 TextWrapIcon,17 TextWrapIcon,
18} from "lucide-react";18} from "lucide-react";
19import {19import { useCallback, useEffect, useMemo, useRef, useState } from "react";
20 type WheelEvent as ReactWheelEvent,
21 useCallback,
22 useEffect,
23 useMemo,
24 useRef,
25 useState,
26} from "react";
27import { useOpenInPreferredEditor } from "../editorPreferences";20import { useOpenInPreferredEditor } from "../editorPreferences";
28import { type DraftId } from "../composerDraftStore";21import { type DraftId } from "../composerDraftStore";
29import { openDiffFilePrimaryAction } from "../diffFileActions";22import { openDiffFilePrimaryAction } from "../diffFileActions";
30import { useCheckpointDiff } from "~/lib/checkpointDiffState";23import { useCheckpointDiff } from "~/lib/checkpointDiffState";
31import { cn } from "~/lib/utils";24import { cn } from "~/lib/utils";
32import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";25import { selectThreadDiffPanelSelection, useDiffPanelStore } from "../diffPanelStore";
33import { useTheme } from "../hooks/useTheme";26import { useTheme } from "../hooks/useTheme";
34import {27import {
35 buildFileDiffRenderKey,28 buildFileDiffRenderKey,
@@ -40,19 +33,41 @@ import { · 4 unmodified lines
40} from "../lib/diffRendering";33} from "../lib/diffRendering";
41import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";34import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
42import { useProject, useThread } from "../state/entities";35import { useProject, useThread } from "../state/entities";
43import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";36import { resolveThreadRouteRef } from "../threadRoutes";
44import { useSettings } from "../hooks/useSettings";37import { useSettings } from "../hooks/useSettings";
45import { formatShortTimestamp } from "../timestampFormat";38import { formatShortTimestamp } from "../timestampFormat";
46import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";39import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
47import { AnnotatableFileDiff } from "./diffs/AnnotatableFileDiff";40import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableFileDiff";
48import { ToggleGroup, Toggle } from "./ui/toggle-group";41import { ToggleGroup, Toggle } from "./ui/toggle-group";
42import { Switch } from "./ui/switch";
43import {
44 Combobox,
45 ComboboxEmpty,
46 ComboboxInput,
47 ComboboxItem,
48 ComboboxList,
49 ComboboxPopup,
50 ComboboxTrigger,
51} from "./ui/combobox";
52import {
53 DropdownMenu,
54 DropdownMenuContent,
55 DropdownMenuItem,
56 DropdownMenuSub,
57 DropdownMenuSubContent,
58 DropdownMenuSubTrigger,
59 DropdownMenuTrigger,
60} from "./ui/menu";
49import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";61import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
50import { useEnvironmentQuery } from "../state/query";62import { useEnvironmentQuery } from "../state/query";
51import { serverEnvironment } from "../state/server";63import { serverEnvironment } from "../state/server";
64import { reviewEnvironment } from "../state/review";
52import { vcsEnvironment } from "../state/vcs";65import { vcsEnvironment } from "../state/vcs";
66import { buildBaseRefChoices, filterBaseRefChoices } from "../lib/baseRefChoices";
53 67
54type DiffRenderMode = "stacked" | "split";68type DiffRenderMode = "stacked" | "split";
55type DiffThemeType = "light" | "dark";69type DiffThemeType = "light" | "dark";
70const AUTOMATIC_BASE_REF = "__automatic_base_ref__";
56 71
57interface CollapsedDiffFilesState {72interface CollapsedDiffFilesState {
58 readonly scopeKey: string | null;73 readonly scopeKey: string | null;
@@ -167,27 +182,24 @@ interface DiffPanelProps { · 108 unmodified lines
167export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";182export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
168 183
169export default function DiffPanel({ mode = "inline", composerDraftTarget }: DiffPanelProps) {184export default function DiffPanel({ mode = "inline", composerDraftTarget }: DiffPanelProps) {
170 const navigate = useNavigate();
171 const { resolvedTheme } = useTheme();185 const { resolvedTheme } = useTheme();
172 const settings = useSettings();186 const settings = useSettings();
173 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");187 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
174 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);188 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);
175 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);189 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
190 const [baseRefQuery, setBaseRefQuery] = useState("");
176 const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({191 const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({
177 scopeKey: null,192 scopeKey: null,
178 fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS,193 fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS,
179 }));194 }));
180 const patchViewportRef = useRef<HTMLDivElement>(null);195 const codeViewRef = useRef<AnnotatableCodeViewHandle>(null);
181 const turnStripRef = useRef<HTMLDivElement>(null);
182 const previousDiffOpenRef = useRef(false);
183 const [canScrollTurnStripLeft, setCanScrollTurnStripLeft] = useState(false);
184 const [canScrollTurnStripRight, setCanScrollTurnStripRight] = useState(false);
185 const routeThreadRef = useParams({196 const routeThreadRef = useParams({
186 strict: false,197 strict: false,
187 select: (params) => resolveThreadRouteRef(params),198 select: (params) => resolveThreadRouteRef(params),
188 });199 });
189 const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) });200 const diffSelection = useDiffPanelStore((state) =>
190 const diffOpen = diffSearch.diff === "1";201 selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef),
202 );
191 const activeThreadId = routeThreadRef?.threadId ?? null;203 const activeThreadId = routeThreadRef?.threadId ?? null;
192 const activeThread = useThread(routeThreadRef);204 const activeThread = useThread(routeThreadRef);
193 const activeProjectId = activeThread?.projectId ?? null;205 const activeProjectId = activeThread?.projectId ?? null;
@@ -233,8 +245,20 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 39 unmodified lines
233 [inferredCheckpointTurnCountByTurnId, turnDiffSummaries],245 [inferredCheckpointTurnCountByTurnId, turnDiffSummaries],
234 );246 );
235 247
236 const selectedTurnId = diffSearch.diffTurnId ?? null;248 useEffect(() => {
237 const selectedFilePath = selectedTurnId !== null ? (diffSearch.diffFilePath ?? null) : null;249 if (!routeThreadRef || diffSelection.kind !== "turn") return;
250 useDiffPanelStore.getState().reconcileTurnSelection(
251 routeThreadRef,
252 orderedTurnDiffSummaries.map((summary) => summary.turnId),
253 );
254 }, [diffSelection, orderedTurnDiffSummaries, routeThreadRef]);
255
256 const selectedTurnId = diffSelection.kind === "turn" ? diffSelection.turnId : null;
257 const selectedGitScope = diffSelection.kind === "unstaged" ? "unstaged" : "branch";
258 const selectedBaseRef = diffSelection.kind === "branch" ? diffSelection.baseRef : null;
259 const selectedFilePath = diffSelection.kind === "turn" ? diffSelection.filePath : null;
260 const selectedFileRevealRequestId =
261 diffSelection.kind === "turn" ? diffSelection.revealRequestId : 0;
238 const selectedTurn =262 const selectedTurn =
239 selectedTurnId === null263 selectedTurnId === null
240 ? undefined264 ? undefined
@@ -243,7 +267,16 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 2 unmodified lines
243 const selectedCheckpointTurnCount =267 const selectedCheckpointTurnCount =
244 selectedTurn &&268 selectedTurn &&
245 (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]);269 (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]);
246 const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : "conversation";270 const latestTurn = orderedTurnDiffSummaries[0];
271 const selectedScopeLabel =
272 selectedTurnId === null
273 ? selectedGitScope === "unstaged"
274 ? "Working tree"
275 : "Branch changes"
276 : selectedTurn?.turnId === latestTurn?.turnId
277 ? "Latest turn"
278 : `Turn ${selectedCheckpointTurnCount ?? "?"}`;
279 const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : selectedGitScope;
247 const collapseScopeKey = routeThreadRef280 const collapseScopeKey = routeThreadRef
248 ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}`281 ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}`
249 : null;282 : null;
@@ -253,7 +286,9 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 3 unmodified lines
253 : EMPTY_COLLAPSED_DIFF_FILE_KEYS;286 : EMPTY_COLLAPSED_DIFF_FILE_KEYS;
254 const reviewSectionTitle = selectedTurn287 const reviewSectionTitle = selectedTurn
255 ? `Turn ${selectedCheckpointTurnCount ?? "?"}`288 ? `Turn ${selectedCheckpointTurnCount ?? "?"}`
256 : "All turns";289 : selectedGitScope === "unstaged"
290 ? "Working tree"
291 : "Branch changes";
257 const selectedCheckpointRange = useMemo(292 const selectedCheckpointRange = useMemo(
258 () =>293 () =>
259 typeof selectedCheckpointTurnCount === "number"294 typeof selectedCheckpointTurnCount === "number"
@@ -264,62 +299,116 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 4 unmodified lines
264 : null,299 : null,
265 [selectedCheckpointTurnCount],300 [selectedCheckpointTurnCount],
266 );301 );
267 const conversationCheckpointTurnCount = useMemo(() => {
268 const turnCounts: Array<number> = [];
269 for (const summary of orderedTurnDiffSummaries) {
270 const value =
271 summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId];
272 if (typeof value === "number") {
273 turnCounts.push(value);
274 }
275 }
276 if (turnCounts.length === 0) {
277 return undefined;
278 }
279 const latest = Math.max(...turnCounts);
280 return latest > 0 ? latest : undefined;
281 }, [inferredCheckpointTurnCountByTurnId, orderedTurnDiffSummaries]);
282 const conversationCheckpointRange = useMemo(
283 () =>
284 !selectedTurn && typeof conversationCheckpointTurnCount === "number"
285 ? {
286 fromTurnCount: 0,
287 toTurnCount: conversationCheckpointTurnCount,
288 }
289 : null,
290 [conversationCheckpointTurnCount, selectedTurn],
291 );
292 const activeCheckpointRange = selectedTurn
293 ? selectedCheckpointRange
294 : conversationCheckpointRange;
295 const conversationCacheScope = useMemo(() => {
296 if (selectedTurn || orderedTurnDiffSummaries.length === 0) {
297 return null;
298 }
299 return `conversation:${orderedTurnDiffSummaries.map((summary) => summary.turnId).join(",")}`;
300 }, [orderedTurnDiffSummaries, selectedTurn]);
301 const activeCheckpointDiff = useCheckpointDiff(302 const activeCheckpointDiff = useCheckpointDiff(
302 {303 {
303 environmentId: activeThread?.environmentId ?? null,304 environmentId: activeThread?.environmentId ?? null,
304 threadId: activeThreadId,305 threadId: activeThreadId,
305 fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null,306 fromTurnCount: selectedCheckpointRange?.fromTurnCount ?? null,
306 toTurnCount: activeCheckpointRange?.toTurnCount ?? null,307 toTurnCount: selectedCheckpointRange?.toTurnCount ?? null,
307 ignoreWhitespace: diffIgnoreWhitespace,308 ignoreWhitespace: diffIgnoreWhitespace,
308 cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope,309 cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : null,
309 },310 },
310 { enabled: isGitRepo },311 { enabled: isGitRepo && selectedTurn !== undefined },
311 );312 );
312 const selectedTurnCheckpointDiff = selectedTurn ? activeCheckpointDiff.data?.diff : undefined;313 const primaryBranchDiffPreview = useEnvironmentQuery(
313 const conversationCheckpointDiff = selectedTurn ? undefined : activeCheckpointDiff.data?.diff;314 selectedTurnId === null && activeThread && activeCwd
314 const isLoadingCheckpointDiff = activeCheckpointDiff.isPending;315 ? reviewEnvironment.diffPreview({
315 const checkpointDiffError = activeCheckpointDiff.error;316 environmentId: activeThread.environmentId,
316 317 input: {
317 const selectedPatch = selectedTurn ? selectedTurnCheckpointDiff : conversationCheckpointDiff;318 cwd: activeCwd,
319 ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}),
320 ignoreWhitespace: diffIgnoreWhitespace,
321 },
322 })
323 : null,
324 );
325 const shouldRetryBranchDiffAtEnvironmentCwd =
326 selectedTurnId === null &&
327 primaryBranchDiffPreview.error?.includes("configured workspace root") === true &&
328 serverConfig?.cwd !== undefined &&
329 serverConfig.cwd !== activeCwd;
330 const fallbackBranchDiffPreview = useEnvironmentQuery(
331 shouldRetryBranchDiffAtEnvironmentCwd && activeThread && serverConfig
332 ? reviewEnvironment.diffPreview({
333 environmentId: activeThread.environmentId,
334 input: {
335 cwd: serverConfig.cwd,
336 ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}),
337 ignoreWhitespace: diffIgnoreWhitespace,
338 },
339 })
340 : null,
341 );
342 const branchDiffPreview = shouldRetryBranchDiffAtEnvironmentCwd
343 ? fallbackBranchDiffPreview
344 : primaryBranchDiffPreview;
345 const selectedGitSource = branchDiffPreview.data?.sources.find(
346 (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"),
347 );
348 const localBranchRefs = useEnvironmentQuery(
349 selectedTurnId === null &&
350 selectedGitScope === "branch" &&
351 activeThread &&
352 branchDiffPreview.data?.cwd
353 ? vcsEnvironment.listRefs({
354 environmentId: activeThread.environmentId,
355 input: {
356 cwd: branchDiffPreview.data.cwd,
357 includeMatchingRemoteRefs: true,
358 refKind: "local",
359 ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}),
360 limit: 100,
361 },
362 })
363 : null,
364 );
365 const remoteBranchRefs = useEnvironmentQuery(
366 selectedTurnId === null &&
367 selectedGitScope === "branch" &&
368 activeThread &&
369 branchDiffPreview.data?.cwd
370 ? vcsEnvironment.listRefs({
371 environmentId: activeThread.environmentId,
372 input: {
373 cwd: branchDiffPreview.data.cwd,
374 includeMatchingRemoteRefs: true,
375 refKind: "remote",
376 ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}),
377 limit: 100,
378 },
379 })
380 : null,
381 );
382 const baseRefChoices = buildBaseRefChoices(
383 localBranchRefs.data?.refs.filter((ref) => ref.name !== selectedGitSource?.headRef) ?? [],
384 remoteBranchRefs.data?.refs ?? [],
385 );
386 const matchingBaseRefChoices = filterBaseRefChoices(baseRefChoices, baseRefQuery);
387 const valueForBaseRefChoice = (choice: (typeof baseRefChoices)[number]) =>
388 selectedBaseRef && selectedBaseRef === choice.remote?.name
389 ? selectedBaseRef
390 : (choice.local?.name ?? choice.remote?.name ?? choice.id);
391 const baseRefItems = [AUTOMATIC_BASE_REF, ...baseRefChoices.map(valueForBaseRefChoice)];
392 const filteredBaseRefItems = [
393 ...(baseRefQuery.trim().length === 0 ? [AUTOMATIC_BASE_REF] : []),
394 ...matchingBaseRefChoices.map(valueForBaseRefChoice),
395 ];
396 const gitDiff = selectedGitSource?.diff;
397
398 const selectedPatch = selectedTurn ? activeCheckpointDiff.data?.diff : gitDiff;
399 const isSelectedPatchTruncated = !selectedTurn && selectedGitSource?.truncated === true;
400 const isLoadingSelectedPatch = selectedTurn
401 ? activeCheckpointDiff.isPending
402 : branchDiffPreview.isPending;
403 const selectedPatchError = selectedTurn ? activeCheckpointDiff.error : branchDiffPreview.error;
318 const hasResolvedPatch = typeof selectedPatch === "string";404 const hasResolvedPatch = typeof selectedPatch === "string";
319 const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0;405 const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0;
320 const renderablePatch = useMemo(406 const renderablePatch = useMemo(
321 () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`),407 () =>
322 [resolvedTheme, selectedPatch],408 getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`, {
409 compactPartialHunkOffsets: selectedTurnId === null,
410 }),
411 [resolvedTheme, selectedPatch, selectedTurnId],
323 );412 );
324 const renderableFiles = useMemo(() => {413 const renderableFiles = useMemo(() => {
325 if (!renderablePatch || renderablePatch.kind !== "files") {414 if (!renderablePatch || renderablePatch.kind !== "files") {
@@ -332,24 +421,26 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 6 unmodified lines
332 }),421 }),
333 );422 );
334 }, [renderablePatch]);423 }, [renderablePatch]);
424 const codeViewFiles = useMemo(
425 () =>
426 renderableFiles.map((fileDiff) => {
427 const fileKey = buildFileDiffRenderKey(fileDiff);
428 return {
429 fileDiff,
430 filePath: resolveFileDiffPath(fileDiff),
431 fileKey,
432 collapsed: collapsedDiffFileKeys.has(fileKey),
433 };
434 }),
435 [collapsedDiffFileKeys, renderableFiles],
436 );
335 437
336 useEffect(() => {438 useEffect(() => {
337 if (diffOpen && !previousDiffOpenRef.current) {439 if (!selectedFilePath) return;
338 setDiffWordWrap(settings.diffWordWrap);440 const file = codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath);
339 setDiffIgnoreWhitespace(settings.diffIgnoreWhitespace);441 if (!file) return;
340 }442 codeViewRef.current?.scrollTo({ type: "item", id: file.fileKey, align: "start" });
341 previousDiffOpenRef.current = diffOpen;443 }, [codeViewFiles, selectedFilePath, selectedFileRevealRequestId]);
342 }, [diffOpen, settings.diffIgnoreWhitespace, settings.diffWordWrap]);
343
344 useEffect(() => {
345 if (!selectedFilePath || !patchViewportRef.current) {
346 return;
347 }
348 const target = Array.from(
349 patchViewportRef.current.querySelectorAll<HTMLElement>("[data-diff-file-path]"),
350 ).find((element) => element.dataset.diffFilePath === selectedFilePath);
351 target?.scrollIntoView({ block: "nearest" });
352 }, [selectedFilePath, renderableFiles]);
353 444
354 const openDiffFile = useCallback(445 const openDiffFile = useCallback(
355 (filePath: string) => {446 (filePath: string) => {
@@ -385,186 +476,190 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 29 unmodified lines
385 );476 );
386 477
387 const selectTurn = (turnId: TurnId) => {478 const selectTurn = (turnId: TurnId) => {
388 if (!activeThread) return;479 if (!routeThreadRef) return;
389 void navigate({480 useDiffPanelStore.getState().selectTurn(routeThreadRef, turnId);
390 to: "/$environmentId/$threadId",
391 params: buildThreadRouteParams(scopeThreadRef(activeThread.environmentId, activeThread.id)),
392 search: (previous) => {
393 const rest = stripDiffSearchParams(previous);
394 return { ...rest, diff: "1", diffTurnId: turnId };
395 },
396 });
397 };481 };
398 const selectWholeConversation = () => {482 const selectGitScope = (scope: "branch" | "unstaged") => {
399 if (!activeThread) return;483 if (!routeThreadRef) return;
400 void navigate({484 useDiffPanelStore.getState().selectGitScope(routeThreadRef, scope);
401 to: "/$environmentId/$threadId",485 };
402 params: buildThreadRouteParams(scopeThreadRef(activeThread.environmentId, activeThread.id)),486 const selectBranchBaseRef = (baseRef: string | null) => {
403 search: (previous) => {487 if (!routeThreadRef) return;
404 const rest = stripDiffSearchParams(previous);488 useDiffPanelStore.getState().selectBranchBaseRef(routeThreadRef, baseRef);
405 return { ...rest, diff: "1" };
406 },
407 });
408 };489 };
409 const updateTurnStripScrollState = useCallback(() => {
410 const element = turnStripRef.current;
411 if (!element) {
412 setCanScrollTurnStripLeft(false);
413 setCanScrollTurnStripRight(false);
414 return;
415 }
416
417 const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
418 setCanScrollTurnStripLeft(element.scrollLeft > 4);
419 setCanScrollTurnStripRight(element.scrollLeft < maxScrollLeft - 4);
420 }, []);
421 const scrollTurnStripBy = useCallback((offset: number) => {
422 const element = turnStripRef.current;
423 if (!element) return;
424 element.scrollBy({ left: offset, behavior: "smooth" });
425 }, []);
426 const onTurnStripWheel = useCallback((event: ReactWheelEvent<HTMLDivElement>) => {
427 const element = turnStripRef.current;
428 if (!element) return;
429 if (element.scrollWidth <= element.clientWidth + 1) return;
430 if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
431
432 event.preventDefault();
433 element.scrollBy({ left: event.deltaY, behavior: "auto" });
434 }, []);
435
436 useEffect(() => {
437 const element = turnStripRef.current;
438 if (!element) return;
439
440 const frameId = window.requestAnimationFrame(() => updateTurnStripScrollState());
441 const onScroll = () => updateTurnStripScrollState();
442
443 element.addEventListener("scroll", onScroll, { passive: true });
444
445 const resizeObserver = new ResizeObserver(() => updateTurnStripScrollState());
446 resizeObserver.observe(element);
447
448 return () => {
449 window.cancelAnimationFrame(frameId);
450 element.removeEventListener("scroll", onScroll);
451 resizeObserver.disconnect();
452 };
453 }, [updateTurnStripScrollState]);
454
455 useEffect(() => {
456 const frameId = window.requestAnimationFrame(() => updateTurnStripScrollState());
457 return () => {
458 window.cancelAnimationFrame(frameId);
459 };
460 }, [orderedTurnDiffSummaries, selectedTurnId, updateTurnStripScrollState]);
461
462 useEffect(() => {
463 const element = turnStripRef.current;
464 if (!element) return;
465
466 const selectedChip = element.querySelector<HTMLElement>("[data-turn-chip-selected='true']");
467 selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
468 }, [selectedTurn?.turnId, selectedTurnId]);
469 490
470 const headerRow = (491 const headerRow = (
471 <>492 <>
472 <div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]">493 <div className="flex min-w-0 flex-1 items-center gap-3 [-webkit-app-region:no-drag]">
473 <button494 <DropdownMenu>
474 type="button"495 <DropdownMenuTrigger
475 className={cn(496 className="inline-flex h-6 max-w-full items-center gap-1 rounded-md bg-muted/70 px-2 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
476 "absolute left-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors",497 aria-label={`Diff scope: ${selectedScopeLabel}`}
477 canScrollTurnStripLeft
478 ? "border-border/70 hover:border-border hover:text-foreground"
479 : "cursor-not-allowed border-border/40 text-muted-foreground/40",
480 )}
481 onClick={() => scrollTurnStripBy(-180)}
482 disabled={!canScrollTurnStripLeft}
483 aria-label="Scroll turn list left"
484 >
485 <ChevronLeftIcon className="size-3.5" />
486 </button>
487 <button
488 type="button"
489 className={cn(
490 "absolute right-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors",
491 canScrollTurnStripRight
492 ? "border-border/70 hover:border-border hover:text-foreground"
493 : "cursor-not-allowed border-border/40 text-muted-foreground/40",
494 )}
495 onClick={() => scrollTurnStripBy(180)}
496 disabled={!canScrollTurnStripRight}
497 aria-label="Scroll turn list right"
498 >
499 <ChevronRightIcon className="size-3.5" />
500 </button>
501 <div
502 ref={turnStripRef}
503 className="turn-chip-strip flex gap-1 overflow-x-auto px-8 py-0.5"
504 style={
505 canScrollTurnStripLeft || canScrollTurnStripRight
506 ? {
507 maskImage: `linear-gradient(to right, ${canScrollTurnStripLeft ? "transparent 24px, black 72px" : "black"}, ${canScrollTurnStripRight ? "black calc(100% - 72px), transparent calc(100% - 24px)" : "black"})`,
508 }
509 : undefined
510 }
511 onWheel={onTurnStripWheel}
512 >
513 <button
514 type="button"
515 className="shrink-0 rounded-md"
516 onClick={selectWholeConversation}
517 data-turn-chip-selected={selectedTurnId === null}
518 >498 >
519 <div499 <span className="truncate">{selectedScopeLabel}</span>
520 className={cn(500 <ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
521 "rounded-md border px-2 py-1 text-left transition-colors",501 </DropdownMenuTrigger>
522 selectedTurnId === null502 <DropdownMenuContent align="start" className="w-60">
523 ? "border-border bg-accent text-accent-foreground"503 <DropdownMenuItem onClick={() => selectGitScope("unstaged")}>
524 : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80",504 <span>Working tree</span>
505 {selectedTurnId === null && selectedGitScope === "unstaged" && (
506 <CheckIcon className="ml-auto" />
507 )}
508 </DropdownMenuItem>
509 <DropdownMenuItem onClick={() => selectGitScope("branch")}>
510 <span>Branch changes</span>
511 {selectedTurnId === null && selectedGitScope === "branch" && (
512 <CheckIcon className="ml-auto" />
525 )}513 )}
514 </DropdownMenuItem>
515 <DropdownMenuItem
516 onClick={() => {
517 if (latestTurn) selectTurn(latestTurn.turnId);
518 }}
526 >519 >
527 <div className="text-[10px] leading-tight font-medium">All turns</div>520 <span>Latest turn</span>
528 </div>521 {selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId && (
529 </button>522 <CheckIcon className="ml-auto" />
530 {orderedTurnDiffSummaries.map((summary) => (523 )}
531 <Tooltip key={summary.turnId}>524 </DropdownMenuItem>
532 <TooltipTrigger525 <DropdownMenuSub>
533 render={526 <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>
534 <button527 <DropdownMenuSubContent className="w-64">
535 type="button"528 {orderedTurnDiffSummaries.map((summary) => {
536 aria-label={summary.turnId}529 const turnCount =
537 className="shrink-0 rounded-md"530 summary.checkpointTurnCount ??
538 onClick={() => selectTurn(summary.turnId)}531 inferredCheckpointTurnCountByTurnId[summary.turnId] ??
539 data-turn-chip-selected={summary.turnId === selectedTurn?.turnId}532 "?";
540 />533 return (
541 }534 <DropdownMenuItem
535 key={summary.turnId}
536 onClick={() => selectTurn(summary.turnId)}
537 >
538 <span>Turn {turnCount}</span>
539 <span className="ml-auto text-xs tabular-nums text-muted-foreground">
540 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
541 </span>
542 {summary.turnId === selectedTurn?.turnId && <CheckIcon className="ml-1" />}
543 </DropdownMenuItem>
544 );
545 })}
546 </DropdownMenuSubContent>
547 </DropdownMenuSub>
548 </DropdownMenuContent>
549 </DropdownMenu>
550 {selectedTurnId === null && selectedGitScope === "branch" && selectedGitSource?.baseRef && (
551 <div
552 className="flex min-w-0 max-w-full items-center gap-2 overflow-hidden text-xs text-muted-foreground"
553 title={`${selectedGitSource.headRef ?? "HEAD"} → ${selectedGitSource.baseRef}`}
554 aria-label={`Comparing ${selectedGitSource.headRef ?? "HEAD"} against ${selectedGitSource.baseRef}`}
555 >
556 <span className="min-w-0 max-w-48 truncate">{selectedGitSource.headRef ?? "HEAD"}</span>
557 <ArrowRightIcon className="size-3.5 shrink-0 opacity-70" />
558 <Combobox
559 items={baseRefItems}
560 filteredItems={filteredBaseRefItems}
561 value={selectedBaseRef ?? AUTOMATIC_BASE_REF}
562 onOpenChange={(open) => {
563 if (!open) setBaseRefQuery("");
564 }}
565 onValueChange={(value) => {
566 if (!value) return;
567 selectBranchBaseRef(value === AUTOMATIC_BASE_REF ? null : value);
568 }}
569 >
570 <ComboboxTrigger
571 className="inline-flex min-w-0 max-w-48 items-center gap-1 overflow-hidden rounded-md px-1.5 py-1 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
572 aria-label={`Change comparison target. Currently ${selectedGitSource.baseRef}`}
573 >
574 <span className="min-w-0 truncate">{selectedGitSource.baseRef}</span>
575 <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" />
576 </ComboboxTrigger>
577 <ComboboxPopup
578 align="start"
579 className="w-72 min-w-0 max-w-[calc(100vw-1rem)] overflow-hidden [&>[data-slot=combobox-popup]]:min-w-0 [&>[data-slot=combobox-popup]]:overflow-hidden"
542 >580 >
543 <div581 <div className="min-w-0 shrink-0 px-3 pt-2.5">
544 className={cn(582 <div className="relative -translate-y-px border-b border-border/70 pb-1.5 transition-colors focus-within:border-ring">
545 "rounded-md border px-2 py-1 text-left transition-colors",583 <SearchIcon
546 summary.turnId === selectedTurn?.turnId584 aria-hidden="true"
547 ? "border-border bg-accent text-accent-foreground"585 className="pointer-events-none absolute top-1.5 left-0 size-4 shrink-0 text-muted-foreground/55"
548 : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80",586 />
549 )}587 <ComboboxInput
550 >588 className="[&_input]:h-6.5 [&_input]:ps-5 [&_input]:font-sans [&_input]:leading-6.5"
551 <div className="flex items-center gap-1">589 inputClassName="rounded-none bg-transparent text-sm"
552 <span className="text-[10px] leading-tight font-medium">590 placeholder="Search refs..."
553 Turn{" "}591 showTrigger={false}
554 {summary.checkpointTurnCount ??592 size="sm"
555 inferredCheckpointTurnCountByTurnId[summary.turnId] ??593 unstyled
556 "?"}594 value={baseRefQuery}
557 </span>595 onChange={(event) => setBaseRefQuery(event.target.value)}
558 <span className="text-[9px] leading-tight opacity-70 tabular-nums">596 />
559 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
560 </span>
561 </div>597 </div>
562 </div>598 </div>
563 </TooltipTrigger>599 <div className="grid shrink-0 grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 border-b border-border/70 ps-3 pe-6.5 pt-2 pb-1.5 font-medium text-[10px] text-muted-foreground uppercase tracking-wide">
564 <TooltipPopup side="top">{summary.turnId}</TooltipPopup>600 <span aria-hidden="true" />
565 </Tooltip>601 <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_2rem] items-center">
566 ))}602 <span>Branch</span>
567 </div>603 <span className="text-right">Remote</span>
604 </div>
605 </div>
606 <ComboboxEmpty>No matching refs.</ComboboxEmpty>
607 <ComboboxList className="max-h-64 min-w-0 overflow-x-hidden">
608 <ComboboxItem
609 className="h-8 w-full min-w-0 grid-cols-[1rem_minmax(0,1fr)] py-0"
610 contentClassName="w-full min-w-0 overflow-hidden"
611 value={AUTOMATIC_BASE_REF}
612 >
613 <span className="block min-w-0 truncate">Automatic</span>
614 </ComboboxItem>
615 {baseRefChoices.map((choice) => {
616 const item = valueForBaseRefChoice(choice);
617 const hasBoth = choice.local !== null && choice.remote !== null;
618 const useRemote = choice.remote?.name === item;
619 return (
620 <ComboboxItem
621 key={choice.id}
622 className="h-8 w-full min-w-0 grid-cols-[1rem_minmax(0,1fr)] py-0"
623 contentClassName="w-full min-w-0 overflow-hidden"
624 value={item}
625 >
626 <div className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_2rem] items-center overflow-hidden">
627 <span className="block min-w-0 truncate pe-2">{choice.label}</span>
628 {hasBoth ? (
629 <div
630 className="flex justify-end"
631 onClick={(event) => event.stopPropagation()}
632 onPointerDown={(event) => event.stopPropagation()}
633 >
634 <Switch
635 aria-label={`Use remote version of ${choice.label}`}
636 checked={useRemote}
637 className="[--thumb-size:--spacing(3)]"
638 onCheckedChange={(checked) => {
639 const nextRef = checked
640 ? choice.remote?.name
641 : choice.local?.name;
642 if (nextRef) selectBranchBaseRef(nextRef);
643 }}
644 />
645 </div>
646 ) : choice.remote ? (
647 <span
648 className="flex justify-end text-muted-foreground"
649 title="Remote only"
650 >
651 <CheckIcon aria-hidden="true" className="size-3" />
652 </span>
653 ) : null}
654 </div>
655 </ComboboxItem>
656 );
657 })}
658 </ComboboxList>
659 </ComboboxPopup>
660 </Combobox>
661 </div>
662 )}
568 </div>663 </div>
569 <div className="flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">664 <div className="flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">
570 <ToggleGroup665 <ToggleGroup
@@ -644,24 +739,35 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 73 unmodified lines
644 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">739 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
645 Turn diffs are unavailable because this project is not a git repository.740 Turn diffs are unavailable because this project is not a git repository.
646 </div>741 </div>
647 ) : orderedTurnDiffSummaries.length === 0 ? (742 ) : selectedTurnId !== null && orderedTurnDiffSummaries.length === 0 ? (
648 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">743 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
649 No completed turns yet.744 No completed turns yet.
650 </div>745 </div>
651 ) : (746 ) : (
652 <>747 <>
653 <div748 <div className="diff-panel-viewport flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
654 ref={patchViewportRef}749 {isSelectedPatchTruncated && (
655 className="diff-panel-viewport min-h-0 min-w-0 flex-1 overflow-hidden"750 <p className="shrink-0 border-b border-border/70 bg-muted/40 px-3 py-1.5 text-[11px] text-muted-foreground">
656 >751 This diff was truncated because it exceeded the preview limit. The changes shown are
657 {checkpointDiffError && !renderablePatch && (752 incomplete.
753 </p>
754 )}
755 {selectedPatchError && !renderablePatch && (
658 <div className="px-3">756 <div className="px-3">
659 <p className="mb-2 text-[11px] text-red-500/80">{checkpointDiffError}</p>757 <p className="mb-2 text-[11px] text-red-500/80">{selectedPatchError}</p>
660 </div>758 </div>
661 )}759 )}
662 {!renderablePatch ? (760 {!renderablePatch ? (
663 isLoadingCheckpointDiff ? (761 isLoadingSelectedPatch ? (
664 <DiffPanelLoadingState label="Loading checkpoint diff..." />762 <DiffPanelLoadingState
763 label={
764 selectedTurn
765 ? "Loading checkpoint diff..."
766 : selectedGitScope === "unstaged"
767 ? "Loading working tree diff..."
768 : "Loading branch diff..."
769 }
770 />
665 ) : (771 ) : (
666 <div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">772 <div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
667 <p>773 <p>
@@ -672,88 +778,73 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff · 4 unmodified lines
672 </div>778 </div>
673 )779 )
674 ) : renderablePatch.kind === "files" ? (780 ) : renderablePatch.kind === "files" ? (
675 <Virtualizer781 <div
676 className="diff-render-surface h-full min-h-0 overflow-auto px-2 pb-2"782 className="min-h-0 flex-1"
677 config={{783 onClickCapture={(event) => {
678 overscrollSize: 600,784 const composedPath = event.nativeEvent.composedPath?.() ?? [];
679 intersectionObserverMargin: 1200,785 const title = composedPath.find(
786 (node): node is HTMLElement =>
787 node instanceof HTMLElement && node.hasAttribute("data-title"),
788 );
789 const filePath = title?.textContent?.trim();
790 if (filePath) openDiffFile(filePath);
680 }}791 }}
681 >792 >
682 {renderableFiles.map((fileDiff) => {793 <AnnotatableCodeView
683 const filePath = resolveFileDiffPath(fileDiff);794 viewerRef={codeViewRef}
684 const fileKey = buildFileDiffRenderKey(fileDiff);795 key={collapseScopeKey ?? reviewSectionId}
685 const themedFileKey = `${fileKey}:${resolvedTheme}`;796 className="diff-render-surface h-full min-h-0 overflow-auto"
686 const collapsed = collapsedDiffFileKeys.has(fileKey);797 files={codeViewFiles}
687 return (798 sectionId={reviewSectionId}
688 <div799 sectionTitle={reviewSectionTitle}
689 key={themedFileKey}800 composerDraftTarget={composerDraftTarget}
690 data-diff-file-path={filePath}801 renderHeaderPrefix={(fileDiff, fileKey, collapsed) => {
691 className="diff-render-file group/diff-file mb-2 rounded-md first:mt-2 last:mb-0"802 const filePath = resolveFileDiffPath(fileDiff);
692 onClickCapture={(event) => {803 return (
693 const nativeEvent = event.nativeEvent as MouseEvent;804 <Tooltip>
694 const composedPath = nativeEvent.composedPath?.() ?? [];805 <TooltipTrigger
695 const clickedHeader = composedPath.some((node) => {806 render={
696 if (!(node instanceof Element)) return false;807 <button
697 return node.hasAttribute("data-title");808 type="button"
698 });809 className={cn(
699 if (!clickedHeader) return;810 "inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 transition-colors hover:bg-foreground/10 focus-visible:outline-hidden",
700 openDiffFile(filePath);811 getDiffCollapseIconClassName(fileDiff),
701 }}
702 >
703 <AnnotatableFileDiff
704 fileDiff={fileDiff}
705 filePath={filePath}
706 sectionId={reviewSectionId}
707 sectionTitle={reviewSectionTitle}
708 composerDraftTarget={composerDraftTarget}
709 renderHeaderPrefix={() => (
710 <Tooltip>
711 <TooltipTrigger
712 render={
713 <button
714 type="button"
715 className={cn(
716 "inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 transition-colors hover:bg-foreground/10 focus-visible:outline-hidden",
717 getDiffCollapseIconClassName(fileDiff),
718 )}
719 aria-label={
720 collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`
721 }
722 aria-expanded={!collapsed}
723 onClick={(event) => {
724 event.stopPropagation();
725 toggleDiffFileCollapsed(fileKey);
726 }}
727 />
728 }
729 >
730 {collapsed ? (
731 <ChevronRightIcon className="size-4" />
732 ) : (
733 <ChevronDownIcon className="size-4" />
734 )}812 )}
735 </TooltipTrigger>813 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}
736 <TooltipPopup side="top">814 aria-expanded={!collapsed}
737 {collapsed ? "Expand diff" : "Collapse diff"}815 onClick={(event) => {
738 </TooltipPopup>816 event.stopPropagation();
739 </Tooltip>817 toggleDiffFileCollapsed(fileKey);
740 )}818 }}
741 options={{819 />
742 collapsed,820 }
743 diffStyle: diffRenderMode === "split" ? "split" : "unified",821 >
744 lineDiffType: "none",822 {collapsed ? (
745 overflow: diffWordWrap ? "wrap" : "scroll",823 <ChevronRightIcon className="size-4" />
746 theme: resolveDiffThemeName(resolvedTheme),824 ) : (
747 themeType: resolvedTheme as DiffThemeType,825 <ChevronDownIcon className="size-4" />
748 unsafeCSS: DIFF_PANEL_UNSAFE_CSS,826 )}
749 }}827 </TooltipTrigger>
750 />828 <TooltipPopup side="top">
751 </div>829 {collapsed ? "Expand diff" : "Collapse diff"}
752 );830 </TooltipPopup>
753 })}831 </Tooltip>
754 </Virtualizer>832 );
833 }}
834 options={{
835 diffStyle: diffRenderMode === "split" ? "split" : "unified",
836 lineDiffType: "none",
837 overflow: diffWordWrap ? "wrap" : "scroll",
838 theme: resolveDiffThemeName(resolvedTheme),
839 themeType: resolvedTheme as DiffThemeType,
840 unsafeCSS: DIFF_PANEL_UNSAFE_CSS,
841 stickyHeaders: true,
842 layout: { paddingTop: 8, paddingBottom: 8, gap: 8 },
843 }}
844 />
845 </div>
755 ) : (846 ) : (
756 <div className="h-full overflow-auto p-2">847 <div className="min-h-0 flex-1 overflow-auto p-2">
757 <div className="space-y-2">848 <div className="space-y-2">
758 <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p>849 <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p>
759 <pre850 <pre
apps/web/src/components/DiffPanelShell.tsx+2−8
@@ -48,14 +48,8 @@ export function DiffPanelShell(props: { · 47 unmodified lines
48export function DiffPanelHeaderSkeleton() {48export function DiffPanelHeaderSkeleton() {
49 return (49 return (
50 <>50 <>
51 <div className="relative min-w-0 flex-1">51 <div className="min-w-0 flex-1">
52 <Skeleton className="absolute left-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />52 <Skeleton className="h-8 w-32 rounded-lg" />
53 <Skeleton className="absolute right-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
54 <div className="flex gap-1 overflow-hidden px-8 py-0.5">
55 <Skeleton className="h-6 w-16 shrink-0 rounded-md" />
56 <Skeleton className="h-6 w-24 shrink-0 rounded-md" />
57 <Skeleton className="h-6 w-24 shrink-0 rounded-md max-sm:hidden" />
58 </div>
59 </div>53 </div>
60 <div className="flex shrink-0 gap-1">54 <div className="flex shrink-0 gap-1">
61 <Skeleton className="size-7 rounded-md" />55 <Skeleton className="size-7 rounded-md" />
apps/web/src/components/diffs/AnnotatableFileDiff.tsx+209−2
@@ -1,14 +1,23 @@
1import type {1import type {
2 AnnotationSide,2 AnnotationSide,
3 CodeViewDiffItem,
4 CodeViewItem,
3 DiffLineAnnotation,5 DiffLineAnnotation,
4 FileDiffMetadata,6 FileDiffMetadata,
5 SelectedLineRange,7 SelectedLineRange,
6} from "@pierre/diffs";8} from "@pierre/diffs";
7import { FileDiff, type FileDiffProps } from "@pierre/diffs/react";9import {
10 CodeView,
11 type CodeViewHandle,
12 type CodeViewProps,
13 FileDiff,
14 type FileDiffProps,
15} from "@pierre/diffs/react";
8import type { ScopedThreadRef } from "@t3tools/contracts";16import type { ScopedThreadRef } from "@t3tools/contracts";
9import { useCallback, useMemo, useState, type ReactNode } from "react";17import { useCallback, useMemo, useState, type ReactNode, type Ref } from "react";
10 18
11import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";19import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
20import { fnv1a32 } from "~/lib/diffRendering";
12import {21import {
13 buildDiffReviewComment,22 buildDiffReviewComment,
14 restoreDiffReviewCommentRange,23 restoreDiffReviewCommentRange,
@@ -31,6 +40,7 @@ interface DiffCommentAnnotationGroup { · 16 unmodified lines
31}40}
32 41
33type DiffCommentLineAnnotation = DiffLineAnnotation<DiffCommentAnnotationGroup>;42type DiffCommentLineAnnotation = DiffLineAnnotation<DiffCommentAnnotationGroup>;
43export type AnnotatableCodeViewHandle = CodeViewHandle<DiffCommentAnnotationGroup>;
34const EMPTY_REVIEW_COMMENTS: ReadonlyArray<ReviewCommentContext> = [];44const EMPTY_REVIEW_COMMENTS: ReadonlyArray<ReviewCommentContext> = [];
35 45
36function annotationSide(range: SelectedLineRange): AnnotationSide {46function annotationSide(range: SelectedLineRange): AnnotationSide {
@@ -237,3 +247,200 @@ export function AnnotatableFileDiff({ · 200 unmodified lines
237 />247 />
238 );248 );
239}249}
250
251interface AnnotatableCodeViewProps {
252 files: ReadonlyArray<{
253 fileDiff: FileDiffMetadata;
254 filePath: string;
255 fileKey: string;
256 collapsed: boolean;
257 }>;
258 sectionId: string;
259 sectionTitle: string;
260 composerDraftTarget: ScopedThreadRef | DraftId;
261 options: NonNullable<CodeViewProps<DiffCommentAnnotationGroup>["options"]>;
262 viewerRef?: Ref<AnnotatableCodeViewHandle>;
263 className?: string;
264 renderHeaderPrefix: (
265 fileDiff: FileDiffMetadata,
266 fileKey: string,
267 collapsed: boolean,
268 ) => ReactNode;
269}
270
271interface DiffSelectionContext {
272 item: CodeViewItem<DiffCommentAnnotationGroup>;
273}
274
275export function AnnotatableCodeView({
276 files,
277 sectionId,
278 sectionTitle,
279 composerDraftTarget,
280 options,
281 viewerRef,
282 className,
283 renderHeaderPrefix,
284}: AnnotatableCodeViewProps) {
285 const addReviewComment = useComposerDraftStore((store) => store.addReviewComment);
286 const removeReviewComment = useComposerDraftStore((store) => store.removeReviewComment);
287 const reviewComments = useComposerDraftStore(
288 (store) => store.getComposerDraft(composerDraftTarget)?.reviewComments ?? EMPTY_REVIEW_COMMENTS,
289 );
290 const [selectedLines, setSelectedLines] = useState<{
291 id: string;
292 range: SelectedLineRange;
293 } | null>(null);
294 const [draft, setDraft] = useState<{
295 fileKey: string;
296 annotation: DiffCommentLineAnnotation;
297 } | null>(null);
298
299 const filesByKey = useMemo(() => new Map(files.map((file) => [file.fileKey, file])), [files]);
300 const items = useMemo<CodeViewDiffItem<DiffCommentAnnotationGroup>[]>(
301 () =>
302 files.map(({ fileDiff, filePath, fileKey, collapsed }) => {
303 const persisted = reviewComments
304 .filter(
305 (comment) =>
306 comment.sectionId === sectionId &&
307 comment.filePath === filePath &&
308 (comment.fenceLanguage ?? "diff") === "diff",
309 )
310 .reduce<DiffCommentLineAnnotation[]>((annotations, comment) => {
311 const range = restoreDiffReviewCommentRange(fileDiff, comment);
312 if (!range) return annotations;
313 return appendAnnotationEntry(annotations, range, {
314 id: comment.id,
315 kind: "comment",
316 range,
317 rangeLabel: comment.rangeLabel,
318 text: comment.text,
319 });
320 }, []);
321 const annotations =
322 draft?.fileKey === fileKey ? [...persisted, draft.annotation] : persisted;
323 return {
324 id: fileKey,
325 type: "diff",
326 fileDiff,
327 annotations,
328 collapsed,
329 version: fnv1a32(
330 `${collapsed ? "1" : "0"}:${annotations
331 .flatMap((annotation) =>
332 annotation.metadata.entries.map(
333 (entry) => `${entry.id}:${entry.rangeLabel}:${entry.text}`,
334 ),
335 )
336 .join(":")}`,
337 ),
338 };
339 }),
340 [draft, files, reviewComments, sectionId],
341 );
342
343 const removeEntry = useCallback(
344 (entryId: string) => {
345 setSelectedLines(null);
346 if (draft?.annotation.metadata.entries.some((entry) => entry.id === entryId)) {
347 setDraft(null);
348 } else {
349 removeReviewComment(composerDraftTarget, entryId);
350 }
351 },
352 [composerDraftTarget, draft, removeReviewComment],
353 );
354
355 const submitEntry = useCallback(
356 (entryId: string, text: string) => {
357 const entry = draft?.annotation.metadata.entries.find(
358 (candidate) => candidate.id === entryId,
359 );
360 const file = draft ? filesByKey.get(draft.fileKey) : undefined;
361 if (!entry || !file) return;
362 const comment = buildDiffReviewComment({
363 id: entry.id,
364 sectionId,
365 sectionTitle,
366 filePath: file.filePath,
367 fileDiff: file.fileDiff,
368 range: entry.range,
369 text,
370 });
371 if (comment) addReviewComment(composerDraftTarget, comment);
372 setSelectedLines(null);
373 setDraft(null);
374 },
375 [addReviewComment, composerDraftTarget, draft, filesByKey, sectionId, sectionTitle],
376 );
377
378 const beginComment = useCallback(
379 (range: SelectedLineRange | null, context: DiffSelectionContext) => {
380 if (!range) return;
381 const item = context.item;
382 if (item.type !== "diff") return;
383 const file = filesByKey.get(item.id);
384 if (!file) return;
385 const id = nextFileCommentId();
386 const comment = buildDiffReviewComment({
387 id,
388 sectionId,
389 sectionTitle,
390 filePath: file.filePath,
391 fileDiff: file.fileDiff,
392 range,
393 text: "",
394 });
395 if (!comment) return;
396 setDraft({
397 fileKey: item.id,
398 annotation: {
399 side: annotationSide(range),
400 lineNumber: range.end,
401 metadata: {
402 entries: [{ id, kind: "draft", range, rangeLabel: comment.rangeLabel, text: "" }],
403 },
404 },
405 });
406 },
407 [filesByKey, sectionId, sectionTitle],
408 );
409
410 const hasOpenComment = draft !== null;
411 return (
412 <CodeView<DiffCommentAnnotationGroup>
413 {...(viewerRef ? { ref: viewerRef } : {})}
414 {...(className ? { className } : {})}
415 items={items}
416 selectedLines={selectedLines}
417 onSelectedLinesChange={setSelectedLines}
418 options={{
419 ...options,
420 enableGutterUtility: !hasOpenComment,
421 enableLineSelection: !hasOpenComment,
422 onLineSelectionEnd: beginComment,
423 }}
424 renderHeaderPrefix={(item) =>
425 item.type === "diff"
426 ? renderHeaderPrefix(item.fileDiff, item.id, item.collapsed === true)
427 : null
428 }
429 renderAnnotation={(annotation) => (
430 <div className="py-1">
431 {annotation.metadata.entries.map((entry) => (
432 <LocalCommentAnnotation
433 key={entry.id}
434 kind={entry.kind}
435 rangeLabel={entry.rangeLabel}
436 text={entry.text}
437 onCancel={() => removeEntry(entry.id)}
438 onComment={(text) => submitEntry(entry.id, text)}
439 onDelete={() => removeEntry(entry.id)}
440 />
441 ))}
442 </div>
443 )}
444 />
445 );
446}
apps/web/src/components/preview/PreviewAutomationOwner.test.ts+2−2
@@ -3,11 +3,11 @@ import { describe, expect, it } from "vite-plus/test"; · 2 unmodified lines
3import { observeAutomationOwnerConnectedGeneration } from "./PreviewAutomationOwner";3import { observeAutomationOwnerConnectedGeneration } from "./PreviewAutomationOwner";
4 4
5describe("observeAutomationOwnerConnectedGeneration", () => {5describe("observeAutomationOwnerConnectedGeneration", () => {
6 it("re-reports ownership only after a later transport generation connects", () => {6 it("reports ownership when the initial transport generation connects", () => {
7 const initial = observeAutomationOwnerConnectedGeneration(null, 1);7 const initial = observeAutomationOwnerConnectedGeneration(null, 1);
8 expect(initial).toEqual({8 expect(initial).toEqual({
9 nextGeneration: 1,9 nextGeneration: 1,
10 shouldReport: false,10 shouldReport: true,
11 });11 });
12 12
13 const disconnected = observeAutomationOwnerConnectedGeneration(initial.nextGeneration, null);13 const disconnected = observeAutomationOwnerConnectedGeneration(initial.nextGeneration, null);
apps/web/src/components/preview/PreviewAutomationOwner.tsx+86−99
@@ -1,15 +1,16 @@
1"use client";1"use client";
2 2
3import { useAtomValue } from "@effect/atom-react";
3import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";4import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
4import type {5import type {
5 PreviewAutomationNavigateInput,6 PreviewAutomationNavigateInput,
6 PreviewAutomationOpenInput,7 PreviewAutomationOpenInput,
7 PreviewAutomationRequest,8 PreviewAutomationRequest,
8 PreviewAutomationResponse,9 PreviewAutomationOwner as PreviewAutomationOwnerState,
9 PreviewAutomationStatus,10 PreviewAutomationStatus,
10 ScopedThreadRef,11 ScopedThreadRef,
11} from "@t3tools/contracts";12} from "@t3tools/contracts";
12import { useCallback, useEffect, useId, useRef } from "react";13import { useCallback, useEffect, useEffectEvent, useId, useMemo, useRef, useState } from "react";
13 14
14import {15import {
15 applyPreviewServerSnapshot,16 applyPreviewServerSnapshot,
@@ -20,11 +21,14 @@ import { useRightPanelStore } from "~/rightPanelStore"; · 4 unmodified lines
20import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver";21import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver";
21import { startBrowserRecording, stopBrowserRecording } from "~/browser/browserRecording";22import { startBrowserRecording, stopBrowserRecording } from "~/browser/browserRecording";
22import { previewEnvironment } from "~/state/preview";23import { previewEnvironment } from "~/state/preview";
23import { useEnvironmentQuery } from "~/state/query";
24import { useEnvironmentConnectionState } from "~/state/environments";24import { useEnvironmentConnectionState } from "~/state/environments";
25import { useAtomCommand } from "~/state/use-atom-command";25import { useAtomCommand } from "~/state/use-atom-command";
26 26
27import { previewBridge } from "./previewBridge";27import { previewBridge } from "./previewBridge";
28import {
29 createLatestPreviewAutomationRequestHandler,
30 createPreviewAutomationRequestConsumerAtom,
31} from "./previewAutomationRequestConsumer";
28 32
29export function observeAutomationOwnerConnectedGeneration(33export function observeAutomationOwnerConnectedGeneration(
30 previousGeneration: number | null,34 previousGeneration: number | null,
@@ -41,7 +45,7 @@ export function observeAutomationOwnerConnectedGeneration( · 10 unmodified lines
41 }45 }
42 return {46 return {
43 nextGeneration: connectedGeneration,47 nextGeneration: connectedGeneration,
44 shouldReport: previousGeneration !== null && previousGeneration !== connectedGeneration,48 shouldReport: previousGeneration !== connectedGeneration,
45 };49 };
46}50}
47 51
@@ -109,24 +113,10 @@ const currentStatus = async ( · 61 unmodified lines
109 };113 };
110};114};
111 115
112const serializeError = (error: unknown): NonNullable<PreviewAutomationResponse["error"]> => {116const previewTabNotFoundError = (): Error => {
113 if (error instanceof Error) {117 const error = new Error("Preview tab is not initialized.");
114 const detail =118 error.name = "PreviewAutomationTabNotFoundError";
115 "detail" in error && (error as { detail?: unknown }).detail !== undefined119 return error;
116 ? (error as { detail?: unknown }).detail
117 : undefined;
118 return {
119 _tag: error.name.startsWith("PreviewAutomation")
120 ? error.name
121 : "PreviewAutomationExecutionError",
122 message: error.message,
123 ...(detail === undefined ? {} : { detail }),
124 };
125 }
126 return {
127 _tag: "PreviewAutomationExecutionError",
128 message: String(error),
129 };
130};120};
131 121
132export function PreviewAutomationOwner(props: {122export function PreviewAutomationOwner(props: {
@@ -135,12 +125,22 @@ export function PreviewAutomationOwner(props: { · 2 unmodified lines
135}) {125}) {
136 const { threadRef, visible } = props;126 const { threadRef, visible } = props;
137 const automationClientId = useId();127 const automationClientId = useId();
138 const automationRequests = useEnvironmentQuery(128 const initialAutomationOwner = useMemo<PreviewAutomationOwnerState>(
139 previewEnvironment.automationRequests({129 () => ({
130 clientId: automationClientId,
140 environmentId: threadRef.environmentId,131 environmentId: threadRef.environmentId,
141 input: { clientId: automationClientId },132 threadId: threadRef.threadId,
133 tabId: null,
134 visible: false,
135 supportsAutomation: Boolean(previewBridge?.automation),
136 focusedAt: new Date().toISOString(),
142 }),137 }),
138 [automationClientId, threadRef.environmentId, threadRef.threadId],
143 );139 );
140 const automationRequestsAtom = previewEnvironment.automationRequests({
141 environmentId: threadRef.environmentId,
142 input: initialAutomationOwner,
143 });
144 const connectionState = useEnvironmentConnectionState(threadRef.environmentId).data;144 const connectionState = useEnvironmentConnectionState(threadRef.environmentId).data;
145 const connectedGeneration =145 const connectedGeneration =
146 connectionState?.phase === "connected" ? connectionState.generation : null;146 connectionState?.phase === "connected" ? connectionState.generation : null;
@@ -159,13 +159,24 @@ export function PreviewAutomationOwner(props: { · 12 unmodified lines
159 previewEnvironment.clearAutomationOwner,159 previewEnvironment.clearAutomationOwner,
160 "preview automation owner clear",160 "preview automation owner clear",
161 );161 );
162 const ownerStateRef = useRef({ threadRef, visible });
163 const connectedGenerationRef = useRef<number | null>(null);162 const connectedGenerationRef = useRef<number | null>(null);
164 const handlerRef = useRef<(request: PreviewAutomationRequest) => Promise<unknown>>(163 const reportCurrentAutomationOwner = useEffectEvent(() => {
165 async () => undefined,164 const state = readThreadPreviewState(threadRef);
166 );165 return reportAutomationOwner({
166 environmentId: threadRef.environmentId,
167 input: {
168 clientId: automationClientId,
169 environmentId: threadRef.environmentId,
170 threadId: threadRef.threadId,
171 tabId: state.snapshot?.tabId ?? null,
172 visible,
173 supportsAutomation: Boolean(previewBridge?.automation),
174 focusedAt: new Date().toISOString(),
175 },
176 });
177 });
167 useEffect(() => {178 useEffect(() => {
168 ownerStateRef.current = { threadRef, visible };179 void reportCurrentAutomationOwner();
169 }, [threadRef, visible]);180 }, [threadRef, visible]);
170 181
171 const handleRequest = useCallback(182 const handleRequest = useCallback(
@@ -208,7 +219,7 @@ export function PreviewAutomationOwner(props: { · 36 unmodified lines
208 return currentStatus(threadRef, input.show ?? true);219 return currentStatus(threadRef, input.show ?? true);
209 }220 }
210 case "navigate": {221 case "navigate": {
211 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");222 if (!previewBridge || !tabId) throw previewTabNotFoundError();
212 const input = request.input as PreviewAutomationNavigateInput;223 const input = request.input as PreviewAutomationNavigateInput;
213 const resolution = resolveBrowserNavigationTarget(224 const resolution = resolveBrowserNavigationTarget(
214 threadRef.environmentId,225 threadRef.environmentId,
@@ -223,46 +234,46 @@ export function PreviewAutomationOwner(props: { · 8 unmodified lines
223 return currentStatus(threadRef, visible);234 return currentStatus(threadRef, visible);
224 }235 }
225 case "snapshot":236 case "snapshot":
226 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");237 if (!previewBridge || !tabId) throw previewTabNotFoundError();
227 return previewBridge.automation.snapshot(tabId);238 return previewBridge.automation.snapshot(tabId);
228 case "click":239 case "click":
229 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");240 if (!previewBridge || !tabId) throw previewTabNotFoundError();
230 return previewBridge.automation.click(241 return previewBridge.automation.click(
231 tabId,242 tabId,
232 request.input as Parameters<typeof previewBridge.automation.click>[1],243 request.input as Parameters<typeof previewBridge.automation.click>[1],
233 );244 );
234 case "type":245 case "type":
235 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");246 if (!previewBridge || !tabId) throw previewTabNotFoundError();
236 return previewBridge.automation.type(247 return previewBridge.automation.type(
237 tabId,248 tabId,
238 request.input as Parameters<typeof previewBridge.automation.type>[1],249 request.input as Parameters<typeof previewBridge.automation.type>[1],
239 );250 );
240 case "press":251 case "press":
241 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");252 if (!previewBridge || !tabId) throw previewTabNotFoundError();
242 return previewBridge.automation.press(253 return previewBridge.automation.press(
243 tabId,254 tabId,
244 request.input as Parameters<typeof previewBridge.automation.press>[1],255 request.input as Parameters<typeof previewBridge.automation.press>[1],
245 );256 );
246 case "scroll":257 case "scroll":
247 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");258 if (!previewBridge || !tabId) throw previewTabNotFoundError();
248 return previewBridge.automation.scroll(259 return previewBridge.automation.scroll(
249 tabId,260 tabId,
250 request.input as Parameters<typeof previewBridge.automation.scroll>[1],261 request.input as Parameters<typeof previewBridge.automation.scroll>[1],
251 );262 );
252 case "evaluate":263 case "evaluate":
253 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");264 if (!previewBridge || !tabId) throw previewTabNotFoundError();
254 return previewBridge.automation.evaluate(265 return previewBridge.automation.evaluate(
255 tabId,266 tabId,
256 request.input as Parameters<typeof previewBridge.automation.evaluate>[1],267 request.input as Parameters<typeof previewBridge.automation.evaluate>[1],
257 );268 );
258 case "waitFor":269 case "waitFor":
259 if (!previewBridge || !tabId) throw new Error("Preview tab is not initialized.");270 if (!previewBridge || !tabId) throw previewTabNotFoundError();
260 return previewBridge.automation.waitFor(271 return previewBridge.automation.waitFor(
261 tabId,272 tabId,
262 request.input as Parameters<typeof previewBridge.automation.waitFor>[1],273 request.input as Parameters<typeof previewBridge.automation.waitFor>[1],
263 );274 );
264 case "recordingStart": {275 case "recordingStart": {
265 if (!tabId) throw new Error("Preview tab is not initialized.");276 if (!tabId) throw previewTabNotFoundError();
266 const startedAt = await startBrowserRecording(tabId);277 const startedAt = await startBrowserRecording(tabId);
267 return {278 return {
268 tabId,279 tabId,
@@ -271,7 +282,7 @@ export function PreviewAutomationOwner(props: { · 2 unmodified lines
271 };282 };
272 }283 }
273 case "recordingStop": {284 case "recordingStop": {
274 if (!tabId) throw new Error("Preview tab is not initialized.");285 if (!tabId) throw previewTabNotFoundError();
275 const artifact = await stopBrowserRecording(tabId);286 const artifact = await stopBrowserRecording(tabId);
276 if (!artifact) throw new Error("No active recording exists for this preview tab.");287 if (!artifact) throw new Error("No active recording exists for this preview tab.");
277 return artifact;288 return artifact;
@@ -280,34 +291,34 @@ export function PreviewAutomationOwner(props: { · 2 unmodified lines
280 },291 },
281 [open, threadRef, visible],292 [open, threadRef, visible],
282 );293 );
294 const [requestHandler] = useState(() =>
295 createLatestPreviewAutomationRequestHandler(handleRequest),
296 );
283 useEffect(() => {297 useEffect(() => {
284 handlerRef.current = handleRequest;298 requestHandler.set(handleRequest);
285 }, [handleRequest]);299 }, [handleRequest, requestHandler]);
286 300
287 useEffect(() => {301 const automationRequestConsumerAtom = useMemo(
288 const request = automationRequests.data;302 () =>
289 if (!request) return;303 createPreviewAutomationRequestConsumerAtom({
290 void handlerRef.current(request).then(304 requestsAtom: automationRequestsAtom,
291 (result) =>305 handleRequest: requestHandler.handle,
292 respondToAutomation({306 respond: (response) =>
293 environmentId: threadRef.environmentId,307 respondToAutomation({
294 input: {308 environmentId: threadRef.environmentId,
295 requestId: request.requestId,309 input: response,
296 ok: true,310 }),
297 ...(result === undefined ? {} : { result }),311 label: `preview:automation-request-consumer:${automationClientId}`,
298 },312 }),
299 }),313 [
300 (error) =>314 automationClientId,
301 respondToAutomation({315 automationRequestsAtom,
302 environmentId: threadRef.environmentId,316 requestHandler,
303 input: {317 respondToAutomation,
304 requestId: request.requestId,318 threadRef.environmentId,
305 ok: false,319 ],
306 error: serializeError(error),320 );
307 },321 useAtomValue(automationRequestConsumerAtom);
308 }),
309 );
310 }, [automationRequests.data, respondToAutomation, threadRef.environmentId]);
311 322
312 useEffect(() => {323 useEffect(() => {
313 const observation = observeAutomationOwnerConnectedGeneration(324 const observation = observeAutomationOwnerConnectedGeneration(
@@ -317,39 +328,11 @@ export function PreviewAutomationOwner(props: { · 3 unmodified lines
317 connectedGenerationRef.current = observation.nextGeneration;328 connectedGenerationRef.current = observation.nextGeneration;
318 if (!observation.shouldReport) return;329 if (!observation.shouldReport) return;
319 330
320 const ownerState = ownerStateRef.current;331 void reportCurrentAutomationOwner();
321 const state = readThreadPreviewState(ownerState.threadRef);332 }, [connectedGeneration]);
322 void reportAutomationOwner({
323 environmentId: ownerState.threadRef.environmentId,
324 input: {
325 clientId: automationClientId,
326 environmentId: ownerState.threadRef.environmentId,
327 threadId: ownerState.threadRef.threadId,
328 tabId: state.snapshot?.tabId ?? null,
329 visible: ownerState.visible,
330 supportsAutomation: Boolean(previewBridge?.automation),
331 focusedAt: new Date().toISOString(),
332 },
333 });
334 }, [automationClientId, connectedGeneration, reportAutomationOwner]);
335 333
336 useEffect(() => {334 useEffect(() => {
337 const report = () => {335 const report = () => void reportCurrentAutomationOwner();
338 const state = readThreadPreviewState(threadRef);
339 void reportAutomationOwner({
340 environmentId: threadRef.environmentId,
341 input: {
342 clientId: automationClientId,
343 environmentId: threadRef.environmentId,
344 threadId: threadRef.threadId,
345 tabId: state.snapshot?.tabId ?? null,
346 visible,
347 supportsAutomation: Boolean(previewBridge?.automation),
348 focusedAt: new Date().toISOString(),
349 },
350 });
351 };
352 report();
353 window.addEventListener("focus", report);336 window.addEventListener("focus", report);
354 const unsubscribe = subscribeThreadPreviewState(threadRef, (state, previous) => {337 const unsubscribe = subscribeThreadPreviewState(threadRef, (state, previous) => {
355 if (state.snapshot?.tabId !== previous.snapshot?.tabId) {338 if (state.snapshot?.tabId !== previous.snapshot?.tabId) {
@@ -361,10 +344,14 @@ export function PreviewAutomationOwner(props: { · 5 unmodified lines
361 unsubscribe();344 unsubscribe();
362 void clearAutomationOwner({345 void clearAutomationOwner({
363 environmentId: threadRef.environmentId,346 environmentId: threadRef.environmentId,
364 input: { clientId: automationClientId },347 input: {
348 clientId: automationClientId,
349 environmentId: threadRef.environmentId,
350 threadId: threadRef.threadId,
351 },
365 });352 });
366 };353 };
367 }, [automationClientId, clearAutomationOwner, reportAutomationOwner, threadRef, visible]);354 }, [automationClientId, clearAutomationOwner, threadRef]);
368 355
369 return null;356 return null;
370}357}
apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts+81−0
@@ -0,0 +1,81 @@
1import type { PreviewAutomationRequest, PreviewAutomationResponse } from "@t3tools/contracts";
2import { ThreadId } from "@t3tools/contracts";
3import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity";
4import { describe, expect, it, vi } from "vite-plus/test";
5
6import {
7 createPreviewAutomationRequestConsumerAtom,
8 serializePreviewAutomationError,
9} from "./previewAutomationRequestConsumer";
10
11const request = (requestId: string): PreviewAutomationRequest => ({
12 requestId,
13 threadId: ThreadId.make("thread-1"),
14 operation: "status",
15 input: {},
16 timeoutMs: 15_000,
17});
18
19describe("previewAutomationRequestConsumer", () => {
20 it("consumes every request emitted before React can render", async () => {
21 const requestsAtom = Atom.make<AsyncResult.AsyncResult<PreviewAutomationRequest, Error>>(
22 AsyncResult.initial<PreviewAutomationRequest, Error>(false),
23 );
24 const handleRequest = vi.fn(async (value: PreviewAutomationRequest) => ({
25 requestId: value.requestId,
26 }));
27 const responses: PreviewAutomationResponse[] = [];
28 const respond = vi.fn(async (response: PreviewAutomationResponse) => {
29 responses.push(response);
30 });
31 const consumerAtom = createPreviewAutomationRequestConsumerAtom({
32 requestsAtom,
33 handleRequest,
34 respond,
35 label: "test:preview-automation-consumer",
36 });
37 const registry = AtomRegistry.make();
38 registry.mount(consumerAtom);
39
40 registry.set(requestsAtom, AsyncResult.success(request("request-1")));
41 registry.set(requestsAtom, AsyncResult.success(request("request-2")));
42
43 await vi.waitFor(() => expect(respond).toHaveBeenCalledTimes(2));
44 expect(handleRequest.mock.calls.map(([value]) => value.requestId)).toEqual([
45 "request-1",
46 "request-2",
47 ]);
48 expect(responses.map((response) => response.requestId)).toEqual(["request-1", "request-2"]);
49 registry.dispose();
50 });
51
52 it("consumes a request that arrived immediately before the consumer mounted", async () => {
53 const requestsAtom = Atom.make(
54 AsyncResult.success<PreviewAutomationRequest, Error>(request("request-ready")),
55 );
56 const respond = vi.fn(async (_response: PreviewAutomationResponse) => undefined);
57 const consumerAtom = createPreviewAutomationRequestConsumerAtom({
58 requestsAtom,
59 handleRequest: async () => undefined,
60 respond,
61 label: "test:preview-automation-initial-request",
62 });
63 const registry = AtomRegistry.make();
64
65 registry.mount(consumerAtom);
66
67 await vi.waitFor(() => expect(respond).toHaveBeenCalledTimes(1));
68 expect(respond).toHaveBeenCalledWith({ requestId: "request-ready", ok: true });
69 registry.dispose();
70 });
71
72 it("preserves typed automation errors in responses", () => {
73 const error = new Error("No preview tab");
74 error.name = "PreviewAutomationTabNotFoundError";
75
76 expect(serializePreviewAutomationError(error)).toEqual({
77 _tag: "PreviewAutomationTabNotFoundError",
78 message: "No preview tab",
79 });
80 });
81});
apps/web/src/components/preview/previewAutomationRequestConsumer.ts+83−0
@@ -0,0 +1,83 @@
1import type { PreviewAutomationRequest, PreviewAutomationResponse } from "@t3tools/contracts";
2import { AsyncResult, Atom } from "effect/unstable/reactivity";
3
4type AutomationRequestResult<E> = AsyncResult.AsyncResult<PreviewAutomationRequest, E>;
5type AutomationRequestHandler = (request: PreviewAutomationRequest) => Promise<unknown>;
6
7export function createLatestPreviewAutomationRequestHandler(initial: AutomationRequestHandler): {
8 readonly set: (handler: AutomationRequestHandler) => void;
9 readonly handle: AutomationRequestHandler;
10} {
11 let current = initial;
12 return {
13 set: (handler) => {
14 current = handler;
15 },
16 handle: (request) => current(request),
17 };
18}
19
20export function serializePreviewAutomationError(
21 error: unknown,
22): NonNullable<PreviewAutomationResponse["error"]> {
23 if (error instanceof Error) {
24 const detail =
25 "detail" in error && (error as { detail?: unknown }).detail !== undefined
26 ? (error as { detail?: unknown }).detail
27 : undefined;
28 return {
29 _tag: error.name.startsWith("PreviewAutomation")
30 ? error.name
31 : "PreviewAutomationExecutionError",
32 message: error.message,
33 ...(detail === undefined ? {} : { detail }),
34 };
35 }
36 return {
37 _tag: "PreviewAutomationExecutionError",
38 message: String(error),
39 };
40}
41
42export function createPreviewAutomationRequestConsumerAtom<E>(options: {
43 readonly requestsAtom: Atom.Atom<AutomationRequestResult<E>>;
44 readonly handleRequest: (request: PreviewAutomationRequest) => Promise<unknown>;
45 readonly respond: (response: PreviewAutomationResponse) => Promise<unknown>;
46 readonly label: string;
47}): Atom.Atom<void> {
48 return Atom.make((get) => {
49 let disposed = false;
50 let requestsVersion = 0;
51
52 const consume = (result: AutomationRequestResult<E>) => {
53 if (!AsyncResult.isSuccess(result)) return;
54 const request = result.value;
55 void options.handleRequest(request).then(
56 (value) =>
57 options.respond({
58 requestId: request.requestId,
59 ok: true,
60 ...(value === undefined ? {} : { result: value }),
61 }),
62 (error) =>
63 options.respond({
64 requestId: request.requestId,
65 ok: false,
66 error: serializePreviewAutomationError(error),
67 }),
68 );
69 };
70
71 get.addFinalizer(() => {
72 disposed = true;
73 });
74 const initialRequest = get.once(options.requestsAtom);
75 get.subscribe(options.requestsAtom, (result) => {
76 requestsVersion += 1;
77 consume(result);
78 });
79 queueMicrotask(() => {
80 if (!disposed && requestsVersion === 0) consume(initialRequest);
81 });
82 }).pipe(Atom.setIdleTTL(0), Atom.withLabel(options.label));
83}
apps/web/src/diffPanelStore.test.ts+68−0
@@ -0,0 +1,68 @@
1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { EnvironmentId, ThreadId, TurnId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vite-plus/test";
4
5import { selectThreadDiffPanelSelection, useDiffPanelStore } from "./diffPanelStore";
6
7const THREAD_REF = scopeThreadRef(EnvironmentId.make("environment-1"), ThreadId.make("thread-1"));
8
9describe("diffPanelStore", () => {
10 beforeEach(() => useDiffPanelStore.setState({ byThreadKey: {}, branchBaseRefByThreadKey: {} }));
11
12 it("defaults each thread to branch changes with automatic base selection", () => {
13 expect(
14 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
15 ).toEqual({ kind: "branch", baseRef: null });
16 });
17
18 it("clears incompatible selection fields when changing scopes", () => {
19 const store = useDiffPanelStore.getState();
20 store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts");
21 store.selectGitScope(THREAD_REF, "unstaged");
22
23 expect(
24 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
25 ).toEqual({ kind: "unstaged" });
26
27 useDiffPanelStore.getState().selectBranchBaseRef(THREAD_REF, " origin/main ");
28 expect(
29 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
30 ).toEqual({ kind: "branch", baseRef: "origin/main" });
31 });
32
33 it("increments the reveal request when opening the same turn file again", () => {
34 const turnId = TurnId.make("turn-1");
35 useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");
36 useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");
37
38 expect(
39 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
40 ).toEqual({ kind: "turn", turnId, filePath: "src/app.ts", revealRequestId: 2 });
41 });
42
43 it("restores the selected branch base after visiting another scope", () => {
44 useDiffPanelStore.getState().selectBranchBaseRef(THREAD_REF, "origin/main");
45 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "unstaged");
46 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");
47
48 expect(
49 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
50 ).toEqual({ kind: "branch", baseRef: "origin/main" });
51 });
52
53 it("reconciles a missing turn selection to the latest available turn", () => {
54 const missingTurnId = TurnId.make("turn-missing");
55 const latestTurnId = TurnId.make("turn-latest");
56 useDiffPanelStore.getState().selectTurn(THREAD_REF, missingTurnId, "src/app.ts");
57 useDiffPanelStore.getState().reconcileTurnSelection(THREAD_REF, [latestTurnId]);
58
59 expect(
60 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
61 ).toEqual({
62 kind: "turn",
63 turnId: latestTurnId,
64 filePath: "src/app.ts",
65 revealRequestId: 1,
66 });
67 });
68});
apps/web/src/diffPanelStore.ts+139−0
@@ -0,0 +1,139 @@
1import { scopedThreadKey } from "@t3tools/client-runtime/environment";
2import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";
3import { create } from "zustand";
4import { createJSONStorage, persist } from "zustand/middleware";
5
6import { resolveStorage } from "./lib/storage";
7
8export type DiffPanelSelection =
9 | { kind: "branch"; baseRef: string | null }
10 | { kind: "unstaged" }
11 | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number };
12
13const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null };
14
15interface DiffPanelStoreState {
16 byThreadKey: Record<string, DiffPanelSelection>;
17 branchBaseRefByThreadKey: Record<string, string | null>;
18 selectGitScope: (ref: ScopedThreadRef, scope: "branch" | "unstaged") => void;
19 selectBranchBaseRef: (ref: ScopedThreadRef, baseRef: string | null) => void;
20 selectTurn: (ref: ScopedThreadRef, turnId: TurnId, filePath?: string) => void;
21 reconcileTurnSelection: (ref: ScopedThreadRef, availableTurnIds: ReadonlyArray<TurnId>) => void;
22 removeThread: (ref: ScopedThreadRef) => void;
23}
24
25function normalizeBaseRef(baseRef: string | null): string | null {
26 const normalized = baseRef?.trim();
27 return normalized ? normalized : null;
28}
29
30export const useDiffPanelStore = create<DiffPanelStoreState>()(
31 persist(
32 (set) => ({
33 byThreadKey: {},
34 branchBaseRefByThreadKey: {},
35 selectGitScope: (ref, scope) =>
36 set((state) => {
37 const threadKey = scopedThreadKey(ref);
38 const previous = state.byThreadKey[threadKey];
39 const previousBaseRef =
40 previous?.kind === "branch"
41 ? previous.baseRef
42 : (state.branchBaseRefByThreadKey[threadKey] ?? null);
43 return {
44 byThreadKey: {
45 ...state.byThreadKey,
46 [threadKey]:
47 scope === "branch"
48 ? { kind: "branch", baseRef: previousBaseRef }
49 : { kind: "unstaged" },
50 },
51 branchBaseRefByThreadKey:
52 previous?.kind === "branch"
53 ? { ...state.branchBaseRefByThreadKey, [threadKey]: previous.baseRef }
54 : state.branchBaseRefByThreadKey,
55 };
56 }),
57 selectBranchBaseRef: (ref, baseRef) =>
58 set((state) => {
59 const threadKey = scopedThreadKey(ref);
60 const normalizedBaseRef = normalizeBaseRef(baseRef);
61 return {
62 byThreadKey: {
63 ...state.byThreadKey,
64 [threadKey]: { kind: "branch", baseRef: normalizedBaseRef },
65 },
66 branchBaseRefByThreadKey: {
67 ...state.branchBaseRefByThreadKey,
68 [threadKey]: normalizedBaseRef,
69 },
70 };
71 }),
72 selectTurn: (ref, turnId, filePath) =>
73 set((state) => {
74 const threadKey = scopedThreadKey(ref);
75 const previous = state.byThreadKey[threadKey];
76 return {
77 byThreadKey: {
78 ...state.byThreadKey,
79 [threadKey]: {
80 kind: "turn",
81 turnId,
82 filePath: filePath?.trim() || null,
83 revealRequestId: previous?.kind === "turn" ? previous.revealRequestId + 1 : 1,
84 },
85 },
86 };
87 }),
88 reconcileTurnSelection: (ref, availableTurnIds) =>
89 set((state) => {
90 const threadKey = scopedThreadKey(ref);
91 const previous = state.byThreadKey[threadKey];
92 const latestTurnId = availableTurnIds[0];
93 if (
94 previous?.kind !== "turn" ||
95 latestTurnId === undefined ||
96 availableTurnIds.includes(previous.turnId)
97 ) {
98 return state;
99 }
100 return {
101 byThreadKey: {
102 ...state.byThreadKey,
103 [threadKey]: { ...previous, turnId: latestTurnId },
104 },
105 };
106 }),
107 removeThread: (ref) =>
108 set((state) => {
109 const threadKey = scopedThreadKey(ref);
110 if (!(threadKey in state.byThreadKey) && !(threadKey in state.branchBaseRefByThreadKey)) {
111 return state;
112 }
113 const { [threadKey]: _removed, ...byThreadKey } = state.byThreadKey;
114 const { [threadKey]: _removedBaseRef, ...branchBaseRefByThreadKey } =
115 state.branchBaseRefByThreadKey;
116 return { byThreadKey, branchBaseRefByThreadKey };
117 }),
118 }),
119 {
120 name: "t3code:diff-panel-state:v1",
121 version: 1,
122 storage: createJSONStorage(() =>
123 resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined),
124 ),
125 partialize: (state) => ({
126 byThreadKey: state.byThreadKey,
127 branchBaseRefByThreadKey: state.branchBaseRefByThreadKey,
128 }),
129 },
130 ),
131);
132
133export function selectThreadDiffPanelSelection(
134 byThreadKey: Record<string, DiffPanelSelection>,
135 ref: ScopedThreadRef | null | undefined,
136): DiffPanelSelection {
137 if (!ref) return DEFAULT_SELECTION;
138 return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;
139}
apps/web/src/diffRouteSearch.test.ts+0−74
@@ -1,74 +0,0 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { parseDiffRouteSearch } from "./diffRouteSearch";
4
5describe("parseDiffRouteSearch", () => {
6 it("parses valid diff search values", () => {
7 const parsed = parseDiffRouteSearch({
8 diff: "1",
9 diffTurnId: "turn-1",
10 diffFilePath: "src/app.ts",
11 });
12
13 expect(parsed).toEqual({
14 diff: "1",
15 diffTurnId: "turn-1",
16 diffFilePath: "src/app.ts",
17 });
18 });
19
20 it("treats numeric and boolean diff toggles as open", () => {
21 expect(
22 parseDiffRouteSearch({
23 diff: 1,
24 diffTurnId: "turn-1",
25 }),
26 ).toEqual({
27 diff: "1",
28 diffTurnId: "turn-1",
29 });
30
31 expect(
32 parseDiffRouteSearch({
33 diff: true,
34 diffTurnId: "turn-1",
35 }),
36 ).toEqual({
37 diff: "1",
38 diffTurnId: "turn-1",
39 });
40 });
41
42 it("drops turn and file values when diff is closed", () => {
43 const parsed = parseDiffRouteSearch({
44 diff: "0",
45 diffTurnId: "turn-1",
46 diffFilePath: "src/app.ts",
47 });
48
49 expect(parsed).toEqual({});
50 });
51
52 it("drops file value when turn is not selected", () => {
53 const parsed = parseDiffRouteSearch({
54 diff: "1",
55 diffFilePath: "src/app.ts",
56 });
57
58 expect(parsed).toEqual({
59 diff: "1",
60 });
61 });
62
63 it("normalizes whitespace-only values", () => {
64 const parsed = parseDiffRouteSearch({
65 diff: "1",
66 diffTurnId: " ",
67 diffFilePath: " ",
68 });
69
70 expect(parsed).toEqual({
71 diff: "1",
72 });
73 });
74});
apps/web/src/diffRouteSearch.ts+0−39
@@ -1,39 +0,0 @@
1import { TurnId } from "@t3tools/contracts";
2
3export interface DiffRouteSearch {
4 diff?: "1" | undefined;
5 diffTurnId?: TurnId | undefined;
6 diffFilePath?: string | undefined;
7}
8
9function isDiffOpenValue(value: unknown): boolean {
10 return value === "1" || value === 1 || value === true;
11}
12
13function normalizeSearchString(value: unknown): string | undefined {
14 if (typeof value !== "string") {
15 return undefined;
16 }
17 const normalized = value.trim();
18 return normalized.length > 0 ? normalized : undefined;
19}
20
21export function stripDiffSearchParams<T extends Record<string, unknown>>(
22 params: T,
23): Omit<T, "diff" | "diffTurnId" | "diffFilePath"> {
24 const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = params;
25 return rest as Omit<T, "diff" | "diffTurnId" | "diffFilePath">;
26}
27
28export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
29 const diff = isDiffOpenValue(search.diff) ? "1" : undefined;
30 const diffTurnIdRaw = diff ? normalizeSearchString(search.diffTurnId) : undefined;
31 const diffTurnId = diffTurnIdRaw ? TurnId.make(diffTurnIdRaw) : undefined;
32 const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;
33
34 return {
35 ...(diff ? { diff } : {}),
36 ...(diffTurnId ? { diffTurnId } : {}),
37 ...(diffFilePath ? { diffFilePath } : {}),
38 };
39}
apps/web/src/environments/primary/httpClient.ts+5−8
@@ -5,16 +5,13 @@ import * as Layer from "effect/Layer"; · 4 unmodified lines
5 5
6import { resolvePrimaryEnvironmentHttpUrl } from "./target";6import { resolvePrimaryEnvironmentHttpUrl } from "./target";
7 7
8export type PrimaryEnvironmentHttpClientShape = Effect.Success<
9 ReturnType<typeof makeEnvironmentHttpApiClient>
10>;
11
12export class PrimaryEnvironmentHttpClient extends Context.Service<8export class PrimaryEnvironmentHttpClient extends Context.Service<
13 PrimaryEnvironmentHttpClient,9 PrimaryEnvironmentHttpClient,
14 PrimaryEnvironmentHttpClientShape10 Effect.Success<ReturnType<typeof makeEnvironmentHttpApiClient>>
15>()("@t3tools/web/environments/primary/httpClient/PrimaryEnvironmentHttpClient") {}11>()("@t3tools/web/environments/primary/httpClient/PrimaryEnvironmentHttpClient") {}
16 12
17export const primaryEnvironmentHttpClientLive = Layer.effect(13const make = Effect.suspend(() =>
18 PrimaryEnvironmentHttpClient,14 makeEnvironmentHttpApiClient(resolvePrimaryEnvironmentHttpUrl("/")),
19 Effect.suspend(() => makeEnvironmentHttpApiClient(resolvePrimaryEnvironmentHttpUrl("/"))),
20);15);
16
17export const layer = Layer.effect(PrimaryEnvironmentHttpClient, make);
apps/web/src/index.css+2−1
@@ -689,7 +689,8 @@ label:has(> select#reasoning-effort) select { · 688 unmodified lines
689 background: color-mix(in srgb, var(--background) 94%, var(--card));689 background: color-mix(in srgb, var(--background) 94%, var(--card));
690}690}
691 691
692.diff-render-file {692.diff-render-file,
693.diff-render-surface > diffs-container {
693 border: 1px solid var(--border);694 border: 1px solid var(--border);
694 border-radius: 0.5rem;695 border-radius: 0.5rem;
695 overflow: clip;696 overflow: clip;
apps/web/src/lib/baseRefChoices.test.ts+52−0
@@ -0,0 +1,52 @@
1import { describe, expect, it } from "vite-plus/test";
2import type { VcsRef } from "@t3tools/contracts";
3import { buildBaseRefChoices, filterBaseRefChoices } from "./baseRefChoices";
4
5function ref(name: string, remoteName?: string): VcsRef {
6 return {
7 name,
8 current: false,
9 isDefault: false,
10 isRemote: remoteName !== undefined,
11 ...(remoteName ? { remoteName } : {}),
12 worktreePath: null,
13 };
14}
15
16describe("buildBaseRefChoices", () => {
17 it("pairs matching local and remote branches and prefers origin", () => {
18 const choices = buildBaseRefChoices(
19 [ref("main")],
20 [ref("upstream/main", "upstream"), ref("origin/main", "origin")],
21 );
22
23 expect(choices).toEqual([
24 expect.objectContaining({
25 label: "main",
26 local: expect.objectContaining({ name: "main" }),
27 remote: expect.objectContaining({ name: "origin/main" }),
28 }),
29 expect.objectContaining({
30 label: "upstream/main",
31 local: null,
32 remote: expect.objectContaining({ name: "upstream/main" }),
33 }),
34 ]);
35 });
36});
37
38describe("filterBaseRefChoices", () => {
39 it("filters stale server results against the current query", () => {
40 const choices = buildBaseRefChoices(
41 [ref("main"), ref("feature/search")],
42 [ref("origin/main", "origin"), ref("origin/feature/search", "origin")],
43 );
44
45 expect(filterBaseRefChoices(choices, "SEARCH").map((choice) => choice.label)).toEqual([
46 "feature/search",
47 ]);
48 expect(filterBaseRefChoices(choices, "origin/main").map((choice) => choice.label)).toEqual([
49 "main",
50 ]);
51 });
52});
apps/web/src/lib/baseRefChoices.ts+61−0
@@ -0,0 +1,61 @@
1import type { VcsRef } from "@t3tools/contracts";
2
3export interface BaseRefChoice {
4 readonly id: string;
5 readonly label: string;
6 readonly local: VcsRef | null;
7 readonly remote: VcsRef | null;
8}
9
10function remoteBranchName(ref: VcsRef): string {
11 if (ref.remoteName && ref.name.startsWith(`${ref.remoteName}/`)) {
12 return ref.name.slice(ref.remoteName.length + 1);
13 }
14 return ref.name;
15}
16
17export function buildBaseRefChoices(
18 localRefs: ReadonlyArray<VcsRef>,
19 remoteRefs: ReadonlyArray<VcsRef>,
20): ReadonlyArray<BaseRefChoice> {
21 const unusedRemoteRefs = new Set(remoteRefs);
22 const pairedChoices = localRefs.map((local) => {
23 const matches = remoteRefs.filter(
24 (remote) => unusedRemoteRefs.has(remote) && remoteBranchName(remote) === local.name,
25 );
26 const remote =
27 matches.find((candidate) => candidate.remoteName === "origin") ?? matches[0] ?? null;
28 if (remote) unusedRemoteRefs.delete(remote);
29 return {
30 id: `local:${local.name}`,
31 label: local.name,
32 local,
33 remote,
34 };
35 });
36
37 const remoteOnlyChoices = remoteRefs
38 .filter((remote) => unusedRemoteRefs.has(remote))
39 .map((remote) => ({
40 id: `remote:${remote.name}`,
41 label: remote.name,
42 local: null,
43 remote,
44 }));
45
46 return [...pairedChoices, ...remoteOnlyChoices];
47}
48
49export function filterBaseRefChoices(
50 choices: ReadonlyArray<BaseRefChoice>,
51 query: string,
52): ReadonlyArray<BaseRefChoice> {
53 const normalizedQuery = query.trim().toLocaleLowerCase();
54 if (normalizedQuery.length === 0) return choices;
55 return choices.filter(
56 (choice) =>
57 choice.label.toLocaleLowerCase().includes(normalizedQuery) ||
58 choice.local?.name.toLocaleLowerCase().includes(normalizedQuery) === true ||
59 choice.remote?.name.toLocaleLowerCase().includes(normalizedQuery) === true,
60 );
61}
apps/web/src/lib/diffRendering.test.ts+54−1
@@ -1,5 +1,5 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import { buildPatchCacheKey } from "./diffRendering";2import { buildPatchCacheKey, getRenderablePatch } from "./diffRendering";
3 3
4describe("buildPatchCacheKey", () => {4describe("buildPatchCacheKey", () => {
5 it("returns a stable cache key for identical content", () => {5 it("returns a stable cache key for identical content", () => {
@@ -29,3 +29,56 @@ describe("buildPatchCacheKey", () => { · 23 unmodified lines
29 );29 );
30 });30 });
31});31});
32
33describe("getRenderablePatch", () => {
34 it("compacts partial hunk render offsets for virtualized review diffs", () => {
35 const patch = [
36 "diff --git a/example.ts b/example.ts",
37 "index 1111111..2222222 100644",
38 "--- a/example.ts",
39 "+++ b/example.ts",
40 "@@ -48,4 +48,4 @@",
41 " context",
42 "-before",
43 "+after",
44 " context",
45 " context",
46 "@@ -80,3 +80,4 @@",
47 " context",
48 "+added",
49 " context",
50 " context",
51 ].join("\n");
52
53 const parsed = getRenderablePatch(patch, "review", {
54 compactPartialHunkOffsets: true,
55 });
56 expect(parsed?.kind).toBe("files");
57 if (parsed?.kind !== "files") return;
58
59 const file = parsed.files[0];
60 expect(file?.hunks[0]?.collapsedBefore).toBe(47);
61 expect(file?.hunks[0]?.unifiedLineStart).toBe(0);
62 expect(file?.hunks[1]?.collapsedBefore).toBeGreaterThan(0);
63 expect(file?.hunks[1]?.unifiedLineStart).toBe(file?.hunks[0]?.unifiedLineCount);
64 expect(file?.unifiedLineCount).toBe(
65 file?.hunks.reduce((total, hunk) => total + hunk.unifiedLineCount, 0),
66 );
67 });
68
69 it("retains source-file offsets for checkpoint diffs", () => {
70 const patch = [
71 "diff --git a/example.ts b/example.ts",
72 "--- a/example.ts",
73 "+++ b/example.ts",
74 "@@ -48,1 +48,1 @@",
75 "-before",
76 "+after",
77 ].join("\n");
78
79 const parsed = getRenderablePatch(patch, "checkpoint");
80 expect(parsed?.kind).toBe("files");
81 if (parsed?.kind !== "files") return;
82 expect(parsed.files[0]?.hunks[0]?.unifiedLineStart).toBe(47);
83 });
84});
apps/web/src/lib/diffRendering.ts+41−1
@@ -52,9 +52,45 @@ export type RenderablePatch = · 51 unmodified lines
52 reason: string;52 reason: string;
53 };53 };
54 54
55interface RenderablePatchOptions {
56 /**
57 * Pierre's partial-patch parser keeps hunk render starts in source-file
58 * coordinates. Its virtualizer iterates partial patches as compact rows, so
59 * review diffs need compact render starts while retaining collapsedBefore
60 * for the "N unmodified lines" separator.
61 */
62 compactPartialHunkOffsets?: boolean;
63}
64
65export function compactPartialHunkOffsets(file: FileDiffMetadata): FileDiffMetadata {
66 if (!file.isPartial) return file;
67
68 let splitLineStart = 0;
69 let unifiedLineStart = 0;
70 const hunks = file.hunks.map((hunk) => {
71 const compactHunk = {
72 ...hunk,
73 splitLineStart,
74 unifiedLineStart,
75 };
76 splitLineStart += hunk.splitLineCount;
77 unifiedLineStart += hunk.unifiedLineCount;
78 return compactHunk;
79 });
80
81 return {
82 ...file,
83 hunks,
84 splitLineCount: splitLineStart,
85 unifiedLineCount: unifiedLineStart,
86 ...(file.cacheKey ? { cacheKey: `${file.cacheKey}:compact-partial` } : {}),
87 };
88}
89
55export function getRenderablePatch(90export function getRenderablePatch(
56 patch: string | undefined,91 patch: string | undefined,
57 cacheScope = "diff-panel",92 cacheScope = "diff-panel",
93 options: RenderablePatchOptions = {},
58): RenderablePatch | null {94): RenderablePatch | null {
59 if (!patch) return null;95 if (!patch) return null;
60 const normalizedPatch = patch.trim();96 const normalizedPatch = patch.trim();
@@ -65,7 +101,11 @@ export function getRenderablePatch( · 4 unmodified lines
65 normalizedPatch,101 normalizedPatch,
66 buildPatchCacheKey(normalizedPatch, cacheScope),102 buildPatchCacheKey(normalizedPatch, cacheScope),
67 );103 );
68 const files = parsedPatches.flatMap((parsedPatch) => parsedPatch.files);104 const files = parsedPatches.flatMap((parsedPatch) =>
105 options.compactPartialHunkOffsets
106 ? parsedPatch.files.map(compactPartialHunkOffsets)
107 : parsedPatch.files,
108 );
69 if (files.length > 0) {109 if (files.length > 0) {
70 return { kind: "files", files };110 return { kind: "files", files };
71 }111 }
apps/web/src/lib/runtime.ts+6−8
@@ -5,10 +5,7 @@ import * as Socket from "effect/unstable/socket/Socket"; · 4 unmodified lines
5 5
6import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";6import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";
7import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";7import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";
8import {8import * as PrimaryEnvironmentHttpClient from "../environments/primary/httpClient";
9 PrimaryEnvironmentHttpClient,
10 primaryEnvironmentHttpClientLive,
11} from "../environments/primary/httpClient";
12import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";9import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
13 10
14import { browserCryptoLayer } from "../cloud/dpop";11import { browserCryptoLayer } from "../cloud/dpop";
@@ -30,11 +27,11 @@ const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig( · 15 unmodified lines
30export const remoteHttpRuntime = ManagedRuntime.make(httpClientLayer);27export const remoteHttpRuntime = ManagedRuntime.make(httpClientLayer);
31 28
32const primaryHttpRuntime = ManagedRuntime.make(29const primaryHttpRuntime = ManagedRuntime.make(
33 primaryEnvironmentHttpClientLive.pipe(Layer.provide(primaryEnvironmentHttpLayer)),30 PrimaryEnvironmentHttpClient.layer.pipe(Layer.provide(primaryEnvironmentHttpLayer)),
34);31);
35 32
36export type PrimaryHttpEffectRunner = <A, E>(33export type PrimaryHttpEffectRunner = <A, E>(
37 effect: Effect.Effect<A, E, PrimaryEnvironmentHttpClient>,34 effect: Effect.Effect<A, E, PrimaryEnvironmentHttpClient.PrimaryEnvironmentHttpClient>,
38) => Promise<A>;35) => Promise<A>;
39 36
40const livePrimaryHttpRunner: PrimaryHttpEffectRunner = (effect) =>37const livePrimaryHttpRunner: PrimaryHttpEffectRunner = (effect) =>
@@ -42,8 +39,9 @@ const livePrimaryHttpRunner: PrimaryHttpEffectRunner = (effect) => · 1 unmodified line
42 39
43let primaryHttpRunner = livePrimaryHttpRunner;40let primaryHttpRunner = livePrimaryHttpRunner;
44 41
45export const runPrimaryHttp = <A, E>(effect: Effect.Effect<A, E, PrimaryEnvironmentHttpClient>) =>42export const runPrimaryHttp = <A, E>(
46 primaryHttpRunner(effect);43 effect: Effect.Effect<A, E, PrimaryEnvironmentHttpClient.PrimaryEnvironmentHttpClient>,
44) => primaryHttpRunner(effect);
47 45
48export function __setPrimaryHttpRunnerForTests(runner?: PrimaryHttpEffectRunner): void {46export function __setPrimaryHttpRunnerForTests(runner?: PrimaryHttpEffectRunner): void {
49 primaryHttpRunner = runner ?? livePrimaryHttpRunner;47 primaryHttpRunner = runner ?? livePrimaryHttpRunner;
apps/web/src/rightPanelStore.test.ts+0−11
@@ -6,7 +6,6 @@ import { · 5 unmodified lines
6 migratePersistedRightPanelState,6 migratePersistedRightPanelState,
7 selectActiveRightPanel,7 selectActiveRightPanel,
8 selectActiveRightPanelSurface,8 selectActiveRightPanelSurface,
9 selectActiveRightPanelKindWithUrl,
10 selectThreadRightPanelState,9 selectThreadRightPanelState,
11 useRightPanelStore,10 useRightPanelStore,
12} from "./rightPanelStore";11} from "./rightPanelStore";
@@ -254,16 +253,6 @@ describe("rightPanelStore", () => { · 241 unmodified lines
254 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("plan");253 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("plan");
255 });254 });
256 255
257 it("?diff=1 always wins over persisted state", () => {
258 useRightPanelStore.getState().open(refA, "preview");
259 expect(
260 selectActiveRightPanelKindWithUrl(useRightPanelStore.getState().byThreadKey, refA, true),
261 ).toBe("diff");
262 expect(
263 selectActiveRightPanelKindWithUrl(useRightPanelStore.getState().byThreadKey, refA, false),
264 ).toBe("preview");
265 });
266
267 it("removeThread clears persisted state", () => {256 it("removeThread clears persisted state", () => {
268 useRightPanelStore.getState().open(refA, "plan");257 useRightPanelStore.getState().open(refA, "plan");
269 useRightPanelStore.getState().removeThread(refA);258 useRightPanelStore.getState().removeThread(refA);
apps/web/src/rightPanelStore.ts+0−10
@@ -550,13 +550,3 @@ export function selectActiveRightPanelSurface( · 549 unmodified lines
550 if (!state.isOpen) return null;550 if (!state.isOpen) return null;
551 return state.surfaces.find((surface) => surface.id === state.activeSurfaceId) ?? null;551 return state.surfaces.find((surface) => surface.id === state.activeSurfaceId) ?? null;
552}552}
553
554export function selectActiveRightPanelKindWithUrl(
555 byThreadKey: Record<string, ThreadRightPanelState>,
556 ref: ScopedThreadRef | null | undefined,
557 diffSearchActive: boolean,
558): RightPanelKind | null {
559 if (!selectThreadRightPanelState(byThreadKey, ref).isOpen) return null;
560 if (diffSearchActive) return "diff";
561 return selectActiveRightPanel(byThreadKey, ref);
562}
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+1−6
@@ -1,10 +1,9 @@
1import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";1import { createFileRoute, useNavigate } from "@tanstack/react-router";
2import { useEffect } from "react";2import { useEffect } from "react";
3 3
4import ChatView from "../components/ChatView";4import ChatView from "../components/ChatView";
5import { threadHasStarted } from "../components/ChatView.logic";5import { threadHasStarted } from "../components/ChatView.logic";
6import { finalizePromotedDraftThreadByRef, useComposerDraftStore } from "../composerDraftStore";6import { finalizePromotedDraftThreadByRef, useComposerDraftStore } from "../composerDraftStore";
7import { type DiffRouteSearch, parseDiffRouteSearch } from "../diffRouteSearch";
8import { resolveThreadRouteRef } from "../threadRoutes";7import { resolveThreadRouteRef } from "../threadRoutes";
9import { SidebarInset } from "~/components/ui/sidebar";8import { SidebarInset } from "~/components/ui/sidebar";
10import { useEnvironmentThreadRefs, useThreadDetail, useThreadShell } from "../state/entities";9import { useEnvironmentThreadRefs, useThreadDetail, useThreadShell } from "../state/entities";
@@ -74,9 +73,5 @@ function ChatThreadRouteView() { · 63 unmodified lines
74}73}
75 74
76export const Route = createFileRoute("/_chat/$environmentId/$threadId")({75export const Route = createFileRoute("/_chat/$environmentId/$threadId")({
77 validateSearch: (search) => parseDiffRouteSearch(search),
78 search: {
79 middlewares: [retainSearchParams<DiffRouteSearch>(["diff"])],
80 },
81 component: ChatThreadRouteView,76 component: ChatThreadRouteView,
82});77});
infra/relay/alchemy.run.ts+3−3
@@ -7,7 +7,7 @@ import * as Effect from "effect/Effect"; · 6 unmodified lines
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Planetscale from "alchemy/Planetscale";8import * as Planetscale from "alchemy/Planetscale";
9 9
10import { PlanetscaleDatabase, RelayHyperdrive } from "./src/db.ts";10import * as RelayDb from "./src/db.ts";
11import { RelayObservability } from "./src/observability.ts";11import { RelayObservability } from "./src/observability.ts";
12import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";12import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";
13import Api from "./src/worker.ts";13import Api from "./src/worker.ts";
@@ -24,8 +24,8 @@ export default Alchemy.Stack( · 10 unmodified lines
24 state: Cloudflare.state(),24 state: Cloudflare.state(),
25 },25 },
26 Effect.gen(function* () {26 Effect.gen(function* () {
27 const db = yield* PlanetscaleDatabase;27 const db = yield* RelayDb.PlanetscaleDatabase;
28 const hyperdrive = yield* RelayHyperdrive;28 const hyperdrive = yield* RelayDb.RelayHyperdrive;
29 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);29 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);
30 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);30 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);
31 const observability = yield* RelayObservability;31 const observability = yield* RelayObservability;
infra/relay/src/Config.ts+19−14
@@ -1,4 +1,5 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
2import * as Redacted from "effect/Redacted";3import * as Redacted from "effect/Redacted";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
4 5
@@ -13,20 +14,24 @@ export interface ApnsCredentials { · 8 unmodified lines
13 readonly environment: ApnsEnvironment;14 readonly environment: ApnsEnvironment;
14}15}
15 16
16export interface RelayConfigurationShape {
17 readonly relayIssuer: string;
18 readonly apns: ApnsCredentials;
19 readonly clerkSecretKey: Redacted.Redacted<string>;
20 readonly clerkPublishableKey: string;
21 readonly clerkJwtAudience: string;
22 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
23 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
24 readonly cloudMintPublicKey: string;
25 readonly managedEndpointBaseDomain: string | undefined;
26 readonly managedEndpointNamespace: string | undefined;
27}
28
29export class RelayConfiguration extends Context.Service<17export class RelayConfiguration extends Context.Service<
30 RelayConfiguration,18 RelayConfiguration,
31 RelayConfigurationShape19 {
20 readonly relayIssuer: string;
21 readonly apns: ApnsCredentials;
22 readonly clerkSecretKey: Redacted.Redacted<string>;
23 readonly clerkPublishableKey: string;
24 readonly clerkJwtAudience: string;
25 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
26 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
27 readonly cloudMintPublicKey: string;
28 readonly managedEndpointBaseDomain: string | undefined;
29 readonly managedEndpointNamespace: string | undefined;
30 }
32>()("t3code-relay/Config/RelayConfiguration") {}31>()("t3code-relay/Config/RelayConfiguration") {}
32
33export const make = (configuration: RelayConfiguration["Service"]) =>
34 RelayConfiguration.of(configuration);
35
36export const layer = (configuration: RelayConfiguration["Service"]) =>
37 Layer.succeed(RelayConfiguration, make(configuration));
infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+2−2
@@ -66,8 +66,8 @@ function makeAgentActivityRows( · 65 unmodified lines
66}66}
67 67
68function makeEnvironmentLinks(68function makeEnvironmentLinks(
69 overrides: Partial<EnvironmentLinks.EnvironmentLinksShape> = {},69 overrides: Partial<EnvironmentLinks.EnvironmentLinks["Service"]> = {},
70): EnvironmentLinks.EnvironmentLinksShape {70): EnvironmentLinks.EnvironmentLinks["Service"] {
71 return {71 return {
72 upsert: () => Effect.void,72 upsert: () => Effect.void,
73 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),73 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),
infra/relay/src/agentActivity/AgentActivityRows.ts+2−2
@@ -9,7 +9,7 @@ import * as Option from "effect/Option"; · 8 unmodified lines
9import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
10import { and, desc, eq, isNull } from "drizzle-orm";10import { and, desc, eq, isNull } from "drizzle-orm";
11 11
12import { RelayDb } from "../db.ts";12import * as RelayDb from "../db.ts";
13import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";13import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";
14 14
15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(
@@ -70,7 +70,7 @@ const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption( · 54 unmodified lines
70);70);
71 71
72const make = Effect.gen(function* () {72const make = Effect.gen(function* () {
73 const db = yield* RelayDb;73 const db = yield* RelayDb.RelayDb;
74 74
75 return AgentActivityRows.of({75 return AgentActivityRows.of({
76 upsert: Effect.fn("relay.agent_activity_rows.upsert")(76 upsert: Effect.fn("relay.agent_activity_rows.upsert")(
infra/relay/src/agentActivity/ApnsClient.ts+6−6
@@ -9,7 +9,7 @@ import * as Option from "effect/Option"; · 8 unmodified lines
9import * as Redacted from "effect/Redacted";9import * as Redacted from "effect/Redacted";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";
12import type { ApnsCredentials } from "../Config.ts";12import * as RelayConfiguration from "../Config.ts";
13import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";13import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";
14 14
15const LIVE_ACTIVITY_NAME = "AgentActivity";15const LIVE_ACTIVITY_NAME = "AgentActivity";
@@ -99,9 +99,9 @@ const encodeApnsJwtPayloadJson = Schema.encodeEffect( · 83 unmodified lines
99);99);
100 100
101const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {101const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {
102 readonly teamId: ApnsCredentials["teamId"];102 readonly teamId: RelayConfiguration.ApnsCredentials["teamId"];
103 readonly keyId: ApnsCredentials["keyId"];103 readonly keyId: RelayConfiguration.ApnsCredentials["keyId"];
104 readonly privateKey: ApnsCredentials["privateKey"];104 readonly privateKey: RelayConfiguration.ApnsCredentials["privateKey"];
105 readonly issuedAtUnixSeconds: number;105 readonly issuedAtUnixSeconds: number;
106}) {106}) {
107 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(107 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(
@@ -235,12 +235,12 @@ export interface ApnsClientShape { · 127 unmodified lines
235 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;235 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;
236 readonly makePushNotificationRequest: typeof makePushNotificationRequest;236 readonly makePushNotificationRequest: typeof makePushNotificationRequest;
237 readonly sendLiveActivityRequest: (input: {237 readonly sendLiveActivityRequest: (input: {
238 readonly credentials: ApnsCredentials;238 readonly credentials: RelayConfiguration.ApnsCredentials;
239 readonly request: ApnsLiveActivityRequest;239 readonly request: ApnsLiveActivityRequest;
240 readonly issuedAtUnixSeconds: number;240 readonly issuedAtUnixSeconds: number;
241 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;241 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
242 readonly sendPushNotificationRequest: (input: {242 readonly sendPushNotificationRequest: (input: {
243 readonly credentials: ApnsCredentials;243 readonly credentials: RelayConfiguration.ApnsCredentials;
244 readonly request: ApnsPushNotificationRequest;244 readonly request: ApnsPushNotificationRequest;
245 readonly issuedAtUnixSeconds: number;245 readonly issuedAtUnixSeconds: number;
246 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;246 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+2−2
@@ -153,7 +153,7 @@ function makeLayer(input: { · 152 unmodified lines
153 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]153 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
154 >;154 >;
155 readonly currentTargets?: ReadonlyArray<LiveActivities.TargetRow>;155 readonly currentTargets?: ReadonlyArray<LiveActivities.TargetRow>;
156 readonly config?: RelayConfiguration.RelayConfigurationShape;156 readonly config?: RelayConfiguration.RelayConfiguration["Service"];
157 readonly execute?: (157 readonly execute?: (
158 request: HttpClientRequest.HttpClientRequest,158 request: HttpClientRequest.HttpClientRequest,
159 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>;159 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>;
@@ -213,7 +213,7 @@ function makeLayer(input: { · 53 unmodified lines
213 input.invalidatedTokens?.push(invalidated);213 input.invalidatedTokens?.push(invalidated);
214 }),214 }),
215 }),215 }),
216 Layer.succeed(RelayConfiguration.RelayConfiguration, input.config ?? config),216 RelayConfiguration.layer(input.config ?? config),
217 input.execute217 input.execute
218 ? Layer.succeed(HttpClient.HttpClient, HttpClient.make(input.execute))218 ? Layer.succeed(HttpClient.HttpClient, HttpClient.make(input.execute))
219 : FetchHttpClient.layer,219 : FetchHttpClient.layer,
infra/relay/src/agentActivity/DeliveryAttempts.test.ts+13−13
@@ -3,7 +3,7 @@ import { describe, expect, it } from "@effect/vitest"; · 2 unmodified lines
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5 5
6import { RelayDb, type RelayDatabase } from "../db.ts";6import * as RelayDb from "../db.ts";
7import { relayDeliveryAttempts } from "../persistence/schema.ts";7import { relayDeliveryAttempts } from "../persistence/schema.ts";
8import * as DeliveryAttempts from "./DeliveryAttempts.ts";8import * as DeliveryAttempts from "./DeliveryAttempts.ts";
9 9
@@ -20,7 +20,7 @@ describe("DeliveryAttempts", () => { · 10 unmodified lines
20 },20 },
21 };21 };
22 },22 },
23 } as unknown as RelayDatabase;23 } as unknown as RelayDb.RelayDb["Service"];
24 24
25 return Effect.gen(function* () {25 return Effect.gen(function* () {
26 const attempts = yield* DeliveryAttempts.DeliveryAttempts;26 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -52,7 +52,7 @@ describe("DeliveryAttempts", () => { · 25 unmodified lines
52 Effect.provide(52 Effect.provide(
53 DeliveryAttempts.layer.pipe(53 DeliveryAttempts.layer.pipe(
54 Layer.provide(NodeCryptoLayer.layer),54 Layer.provide(NodeCryptoLayer.layer),
55 Layer.provide(Layer.succeed(RelayDb, fakeDb)),55 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
56 ),56 ),
57 ),57 ),
58 );58 );
@@ -78,7 +78,7 @@ describe("DeliveryAttempts", () => { · 19 unmodified lines
78 },78 },
79 };79 };
80 },80 },
81 } as unknown as RelayDatabase;81 } as unknown as RelayDb.RelayDb["Service"];
82 82
83 return Effect.gen(function* () {83 return Effect.gen(function* () {
84 const attempts = yield* DeliveryAttempts.DeliveryAttempts;84 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -104,7 +104,7 @@ describe("DeliveryAttempts", () => { · 19 unmodified lines
104 Effect.provide(104 Effect.provide(
105 DeliveryAttempts.layer.pipe(105 DeliveryAttempts.layer.pipe(
106 Layer.provide(NodeCryptoLayer.layer),106 Layer.provide(NodeCryptoLayer.layer),
107 Layer.provide(Layer.succeed(RelayDb, fakeDb)),107 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
108 ),108 ),
109 ),109 ),
110 );110 );
@@ -135,7 +135,7 @@ describe("DeliveryAttempts", () => { · 24 unmodified lines
135 }),135 }),
136 }),136 }),
137 }),137 }),
138 } as unknown as RelayDatabase;138 } as unknown as RelayDb.RelayDb["Service"];
139 139
140 return Effect.gen(function* () {140 return Effect.gen(function* () {
141 const attempts = yield* DeliveryAttempts.DeliveryAttempts;141 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -154,7 +154,7 @@ describe("DeliveryAttempts", () => { · 12 unmodified lines
154 Effect.provide(154 Effect.provide(
155 DeliveryAttempts.layer.pipe(155 DeliveryAttempts.layer.pipe(
156 Layer.provide(NodeCryptoLayer.layer),156 Layer.provide(NodeCryptoLayer.layer),
157 Layer.provide(Layer.succeed(RelayDb, fakeDb)),157 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
158 ),158 ),
159 ),159 ),
160 );160 );
@@ -185,7 +185,7 @@ describe("DeliveryAttempts", () => { · 24 unmodified lines
185 }),185 }),
186 }),186 }),
187 }),187 }),
188 } as unknown as RelayDatabase;188 } as unknown as RelayDb.RelayDb["Service"];
189 189
190 return Effect.gen(function* () {190 return Effect.gen(function* () {
191 const attempts = yield* DeliveryAttempts.DeliveryAttempts;191 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -204,7 +204,7 @@ describe("DeliveryAttempts", () => { · 12 unmodified lines
204 Effect.provide(204 Effect.provide(
205 DeliveryAttempts.layer.pipe(205 DeliveryAttempts.layer.pipe(
206 Layer.provide(NodeCryptoLayer.layer),206 Layer.provide(NodeCryptoLayer.layer),
207 Layer.provide(Layer.succeed(RelayDb, fakeDb)),207 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
208 ),208 ),
209 ),209 ),
210 );210 );
@@ -246,7 +246,7 @@ describe("DeliveryAttempts", () => { · 35 unmodified lines
246 };246 };
247 },247 },
248 }),248 }),
249 } as unknown as RelayDatabase;249 } as unknown as RelayDb.RelayDb["Service"];
250 250
251 return Effect.gen(function* () {251 return Effect.gen(function* () {
252 const attempts = yield* DeliveryAttempts.DeliveryAttempts;252 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -266,7 +266,7 @@ describe("DeliveryAttempts", () => { · 13 unmodified lines
266 Effect.provide(266 Effect.provide(
267 DeliveryAttempts.layer.pipe(267 DeliveryAttempts.layer.pipe(
268 Layer.provide(NodeCryptoLayer.layer),268 Layer.provide(NodeCryptoLayer.layer),
269 Layer.provide(Layer.succeed(RelayDb, fakeDb)),269 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
270 ),270 ),
271 ),271 ),
272 );272 );
@@ -290,7 +290,7 @@ describe("DeliveryAttempts", () => { · 17 unmodified lines
290 },290 },
291 };291 };
292 },292 },
293 } as unknown as RelayDatabase;293 } as unknown as RelayDb.RelayDb["Service"];
294 294
295 return Effect.gen(function* () {295 return Effect.gen(function* () {
296 const attempts = yield* DeliveryAttempts.DeliveryAttempts;296 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
@@ -315,7 +315,7 @@ describe("DeliveryAttempts", () => { · 18 unmodified lines
315 Effect.provide(315 Effect.provide(
316 DeliveryAttempts.layer.pipe(316 DeliveryAttempts.layer.pipe(
317 Layer.provide(NodeCryptoLayer.layer),317 Layer.provide(NodeCryptoLayer.layer),
318 Layer.provide(Layer.succeed(RelayDb, fakeDb)),318 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
319 ),319 ),
320 ),320 ),
321 );321 );
infra/relay/src/agentActivity/DeliveryAttempts.ts+2−2
@@ -7,7 +7,7 @@ import { and, eq, isNull } from "drizzle-orm"; · 6 unmodified lines
7import * as Crypto from "effect/Crypto";7import * as Crypto from "effect/Crypto";
8import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
9 9
10import { RelayDb } from "../db.ts";10import * as RelayDb from "../db.ts";
11import { relayDeliveryAttempts } from "../persistence/schema.ts";11import { relayDeliveryAttempts } from "../persistence/schema.ts";
12 12
13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(
@@ -84,7 +84,7 @@ function insertValues( · 70 unmodified lines
84}84}
85 85
86const make = Effect.gen(function* () {86const make = Effect.gen(function* () {
87 const db = yield* RelayDb;87 const db = yield* RelayDb.RelayDb;
88 const crypto = yield* Crypto.Crypto;88 const crypto = yield* Crypto.Crypto;
89 89
90 const isExpiredClaim = (claimedAt: string | null, now: DateTime.DateTime) => {90 const isExpiredClaim = (claimedAt: string | null, now: DateTime.DateTime) => {
infra/relay/src/agentActivity/Devices.test.ts+13−7
@@ -5,7 +5,7 @@ import { PgDialect } from "drizzle-orm/pg-core"; · 4 unmodified lines
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7 7
8import { RelayDb, type RelayDatabase } from "../db.ts";8import * as RelayDb from "../db.ts";
9import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";9import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
10import * as Devices from "./Devices.ts";10import * as Devices from "./Devices.ts";
11 11
@@ -71,7 +71,7 @@ describe("Devices", () => { · 59 unmodified lines
71 },71 },
72 };72 };
73 },73 },
74 } as unknown as RelayDatabase;74 } as unknown as RelayDb.RelayDb["Service"];
75 75
76 return Effect.gen(function* () {76 return Effect.gen(function* () {
77 const devices = yield* Devices.Devices;77 const devices = yield* Devices.Devices;
@@ -110,7 +110,9 @@ describe("Devices", () => { · 32 unmodified lines
110 pushToStartToken: "push-to-start-token",110 pushToStartToken: "push-to-start-token",
111 }),111 }),
112 ]);112 ]);
113 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));113 }).pipe(
114 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
115 );
114 });116 });
115 117
116 it.effect("unregisters APNs state only for the current user device", () => {118 it.effect("unregisters APNs state only for the current user device", () => {
@@ -130,7 +132,7 @@ describe("Devices", () => { · 13 unmodified lines
130 },132 },
131 };133 };
132 },134 },
133 } as unknown as RelayDatabase;135 } as unknown as RelayDb.RelayDb["Service"];
134 136
135 return Effect.gen(function* () {137 return Effect.gen(function* () {
136 const devices = yield* Devices.Devices;138 const devices = yield* Devices.Devices;
@@ -156,7 +158,9 @@ describe("Devices", () => { · 19 unmodified lines
156 params: ["user-2", "device-1"],158 params: ["user-2", "device-1"],
157 },159 },
158 ]);160 ]);
159 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));161 }).pipe(
162 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
163 );
160 });164 });
161 165
162 it.effect("lists safe notification state without exposing APNs tokens", () => {166 it.effect("lists safe notification state without exposing APNs tokens", () => {
@@ -184,7 +188,7 @@ describe("Devices", () => { · 21 unmodified lines
184 };188 };
185 },189 },
186 }),190 }),
187 } as unknown as RelayDatabase;191 } as unknown as RelayDb.RelayDb["Service"];
188 192
189 return Effect.gen(function* () {193 return Effect.gen(function* () {
190 const devices = yield* Devices.Devices;194 const devices = yield* Devices.Devices;
@@ -215,6 +219,8 @@ describe("Devices", () => { · 24 unmodified lines
215 updatedAt: "2026-06-01T00:00:00.000Z",219 updatedAt: "2026-06-01T00:00:00.000Z",
216 },220 },
217 ]);221 ]);
218 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));222 }).pipe(
223 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
224 );
219 });225 });
220});226});
infra/relay/src/agentActivity/Devices.ts+2−2
@@ -10,7 +10,7 @@ import * as Schema from "effect/Schema"; · 9 unmodified lines
10import { and, eq } from "drizzle-orm";10import { and, eq } from "drizzle-orm";
11import { sql } from "drizzle-orm";11import { sql } from "drizzle-orm";
12 12
13import { RelayDb } from "../db.ts";13import * as RelayDb from "../db.ts";
14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
15 15
16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(
@@ -59,7 +59,7 @@ export class Devices extends Context.Service<Devices, DevicesShape>()( · 42 unmodified lines
59) {}59) {}
60 60
61const make = Effect.gen(function* () {61const make = Effect.gen(function* () {
62 const db = yield* RelayDb;62 const db = yield* RelayDb.RelayDb;
63 63
64 return Devices.of({64 return Devices.of({
65 register: Effect.fn("relay.devices.register")(65 register: Effect.fn("relay.devices.register")(
infra/relay/src/agentActivity/LiveActivities.test.ts+9−5
@@ -8,7 +8,7 @@ import { PgDialect } from "drizzle-orm/pg-core"; · 7 unmodified lines
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10 10
11import { RelayDb, type RelayDatabase } from "../db.ts";11import * as RelayDb from "../db.ts";
12import { relayLiveActivities } from "../persistence/schema.ts";12import { relayLiveActivities } from "../persistence/schema.ts";
13import * as LiveActivities from "./LiveActivities.ts";13import * as LiveActivities from "./LiveActivities.ts";
14 14
@@ -88,7 +88,7 @@ describe("LiveActivities", () => { · 73 unmodified lines
88 },88 },
89 };89 };
90 },90 },
91 } as unknown as RelayDatabase;91 } as unknown as RelayDb.RelayDb["Service"];
92 92
93 return Effect.gen(function* () {93 return Effect.gen(function* () {
94 const liveActivities = yield* LiveActivities.LiveActivities;94 const liveActivities = yield* LiveActivities.LiveActivities;
@@ -138,7 +138,9 @@ describe("LiveActivities", () => { · 43 unmodified lines
138 }),138 }),
139 );139 );
140 }).pipe(140 }).pipe(
141 Effect.provide(LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))),141 Effect.provide(
142 LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
143 ),
142 );144 );
143 },145 },
144 );146 );
@@ -164,7 +166,7 @@ describe("LiveActivities", () => { · 19 unmodified lines
164 },166 },
165 };167 };
166 },168 },
167 } as unknown as RelayDatabase;169 } as unknown as RelayDb.RelayDb["Service"];
168 170
169 return Effect.gen(function* () {171 return Effect.gen(function* () {
170 const liveActivities = yield* LiveActivities.LiveActivities;172 const liveActivities = yield* LiveActivities.LiveActivities;
@@ -190,7 +192,9 @@ describe("LiveActivities", () => { · 19 unmodified lines
190 }),192 }),
191 );193 );
192 }).pipe(194 }).pipe(
193 Effect.provide(LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))),195 Effect.provide(
196 LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
197 ),
194 );198 );
195 });199 });
196});200});
infra/relay/src/agentActivity/LiveActivities.ts+2−2
@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer"; · 11 unmodified lines
12import * as Schema from "effect/Schema";12import * as Schema from "effect/Schema";
13import { and, eq, sql } from "drizzle-orm";13import { and, eq, sql } from "drizzle-orm";
14 14
15import { RelayDb } from "../db.ts";15import * as RelayDb from "../db.ts";
16import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";16import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
17 17
18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(
@@ -108,7 +108,7 @@ const encodeRelayAgentActivityAggregateStateJson = Schema.encodeEffect( · 89 unmodified lines
108);108);
109 109
110const make = Effect.gen(function* () {110const make = Effect.gen(function* () {
111 const db = yield* RelayDb;111 const db = yield* RelayDb.RelayDb;
112 112
113 return LiveActivities.of({113 return LiveActivities.of({
114 register: Effect.fn("relay.live_activities.register")(114 register: Effect.fn("relay.live_activities.register")(
infra/relay/src/agentActivity/MobileRegistrations.test.ts+3−3
@@ -86,8 +86,8 @@ function makeAgentActivityRows( · 85 unmodified lines
86}86}
87 87
88function makeEnvironmentLinks(88function makeEnvironmentLinks(
89 overrides: Partial<EnvironmentLinks.EnvironmentLinksShape> = {},89 overrides: Partial<EnvironmentLinks.EnvironmentLinks["Service"]> = {},
90): EnvironmentLinks.EnvironmentLinksShape {90): EnvironmentLinks.EnvironmentLinks["Service"] {
91 return {91 return {
92 upsert: () => Effect.void,92 upsert: () => Effect.void,
93 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),93 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),
@@ -153,7 +153,7 @@ function makeRegistrationReplayLayer(input: { · 59 unmodified lines
153 Layer.succeed(EnvironmentLinks.EnvironmentLinks, makeEnvironmentLinks()),153 Layer.succeed(EnvironmentLinks.EnvironmentLinks, makeEnvironmentLinks()),
154 Layer.succeed(LiveActivities.LiveActivities, input.liveActivities),154 Layer.succeed(LiveActivities.LiveActivities, input.liveActivities),
155 Layer.succeed(DeliveryAttempts.DeliveryAttempts, makeDeliveryAttempts()),155 Layer.succeed(DeliveryAttempts.DeliveryAttempts, makeDeliveryAttempts()),
156 Layer.succeed(RelayConfiguration.RelayConfiguration, config),156 RelayConfiguration.layer(config),
157 Layer.succeed(ApnsDeliveryQueue.ApnsDeliveryQueueSender, {157 Layer.succeed(ApnsDeliveryQueue.ApnsDeliveryQueueSender, {
158 send: (body) =>158 send: (body) =>
159 Effect.sync(() => {159 Effect.sync(() => {
infra/relay/src/auth/DpopProofs.test.ts+9−5
@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; · 3 unmodified lines
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6 6
7import { RelayDb, type RelayDatabase } from "../db.ts";7import * as RelayDb from "../db.ts";
8import { relayDpopProofs } from "../persistence/schema.ts";8import { relayDpopProofs } from "../persistence/schema.ts";
9import * as DpopProofs from "./DpopProofs.ts";9import * as DpopProofs from "./DpopProofs.ts";
10 10
@@ -41,7 +41,7 @@ describe("DpopProofReplay", () => { · 30 unmodified lines
41 },41 },
42 };42 };
43 },43 },
44 } as unknown as RelayDatabase;44 } as unknown as RelayDb.RelayDb["Service"];
45 45
46 return Effect.gen(function* () {46 return Effect.gen(function* () {
47 const replay = yield* DpopProofs.DpopProofReplay;47 const replay = yield* DpopProofs.DpopProofReplay;
@@ -67,7 +67,9 @@ describe("DpopProofReplay", () => { · 19 unmodified lines
67 expiresAt: "2026-05-25T12:00:00.000Z",67 expiresAt: "2026-05-25T12:00:00.000Z",
68 },68 },
69 ]);69 ]);
70 }).pipe(Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));70 }).pipe(
71 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
72 );
71 });73 });
72 74
73 it.effect("prunes expired proof rows from the maintenance path", () => {75 it.effect("prunes expired proof rows from the maintenance path", () => {
@@ -84,12 +86,14 @@ describe("DpopProofReplay", () => { · 10 unmodified lines
84 },86 },
85 };87 };
86 },88 },
87 } as unknown as RelayDatabase;89 } as unknown as RelayDb.RelayDb["Service"];
88 90
89 return Effect.gen(function* () {91 return Effect.gen(function* () {
90 const replay = yield* DpopProofs.DpopProofReplay;92 const replay = yield* DpopProofs.DpopProofReplay;
91 yield* replay.pruneExpired;93 yield* replay.pruneExpired;
92 expect(calls).toEqual(["delete", "delete.where"]);94 expect(calls).toEqual(["delete", "delete.where"]);
93 }).pipe(Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));95 }).pipe(
96 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
97 );
94 });98 });
95});99});
infra/relay/src/auth/DpopProofs.ts+25−28
@@ -7,7 +7,7 @@ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError"; · 6 unmodified lines
7import { lt } from "drizzle-orm";7import { lt } from "drizzle-orm";
8 8
9import { verifyDpopProof } from "@t3tools/shared/dpop";9import { verifyDpopProof } from "@t3tools/shared/dpop";
10import { RelayDb } from "../db.ts";10import * as RelayDb from "../db.ts";
11import { relayDpopProofs } from "../persistence/schema.ts";11import { relayDpopProofs } from "../persistence/schema.ts";
12 12
13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(
@@ -21,34 +21,31 @@ export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<Dpo · 7 unmodified lines
21 }21 }
22}22}
23 23
24export interface DpopProofReplayShape {24export class DpopProofReplay extends Context.Service<
25 readonly verifyAndConsume: (input: {25 DpopProofReplay,
26 readonly proof: string | undefined;26 {
27 readonly method: string;27 readonly verifyAndConsume: (input: {
28 readonly url: string;28 readonly proof: string | undefined;
29 readonly expectedThumbprint?: string;29 readonly method: string;
30 readonly expectedAccessToken?: string;30 readonly url: string;
31 readonly now: DateTime.DateTime;31 readonly expectedThumbprint?: string;
32 }) => Effect.Effect<string, HttpApiError.Unauthorized | DpopProofReplayPersistenceError>;32 readonly expectedAccessToken?: string;
33 33 readonly now: DateTime.DateTime;
34 readonly consume: (input: {34 }) => Effect.Effect<string, HttpApiError.Unauthorized | DpopProofReplayPersistenceError>;
35 readonly thumbprint: string;35 readonly consume: (input: {
36 readonly jti: string;36 readonly thumbprint: string;
37 readonly iat: number;37 readonly jti: string;
38 readonly expiresAt: DateTime.DateTime;38 readonly iat: number;
39 }) => Effect.Effect<boolean, DpopProofReplayPersistenceError>;39 readonly expiresAt: DateTime.DateTime;
40 40 }) => Effect.Effect<boolean, DpopProofReplayPersistenceError>;
41 readonly pruneExpired: Effect.Effect<void, DpopProofReplayPersistenceError>;41 readonly pruneExpired: Effect.Effect<void, DpopProofReplayPersistenceError>;
42}42 }
43 43>()("t3code-relay/auth/DpopProofs/DpopProofReplay") {}
44export class DpopProofReplay extends Context.Service<DpopProofReplay, DpopProofReplayShape>()(
45 "t3code-relay/auth/DpopProofs/DpopProofReplay",
46) {}
47 44
48const make = Effect.gen(function* () {45const make = Effect.gen(function* () {
49 const db = yield* RelayDb;46 const db = yield* RelayDb.RelayDb;
50 47
51 const consume: DpopProofReplayShape["consume"] = Effect.fn("relay.dpop_proofs.consume")(48 const consume: DpopProofReplay["Service"]["consume"] = Effect.fn("relay.dpop_proofs.consume")(
52 function* (input) {49 function* (input) {
53 const createdAt = DateTime.formatIso(yield* DateTime.now);50 const createdAt = DateTime.formatIso(yield* DateTime.now);
54 const inserted = yield* db51 const inserted = yield* db
@@ -67,7 +64,7 @@ const make = Effect.gen(function* () { · 12 unmodified lines
67 Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })),64 Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })),
68 );65 );
69 66
70 const verifyAndConsume: DpopProofReplayShape["verifyAndConsume"] = Effect.fn(67 const verifyAndConsume: DpopProofReplay["Service"]["verifyAndConsume"] = Effect.fn(
71 "relay.dpop_proofs.verify_and_consume",68 "relay.dpop_proofs.verify_and_consume",
72 )(function* (input) {69 )(function* (input) {
73 yield* Effect.annotateCurrentSpan({70 yield* Effect.annotateCurrentSpan({
@@ -114,7 +111,7 @@ const make = Effect.gen(function* () { · 40 unmodified lines
114 return result.thumbprint;111 return result.thumbprint;
115 });112 });
116 113
117 const pruneExpired: DpopProofReplayShape["pruneExpired"] = Effect.gen(function* () {114 const pruneExpired: DpopProofReplay["Service"]["pruneExpired"] = Effect.gen(function* () {
118 const now = DateTime.formatIso(yield* DateTime.now);115 const now = DateTime.formatIso(yield* DateTime.now);
119 yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now });116 yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now });
120 yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now));117 yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now));
infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts+3−3
@@ -10,7 +10,7 @@ import * as DateTime from "effect/DateTime"; · 9 unmodified lines
10import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";11import * as Layer from "effect/Layer";
12 12
13import { RelayDb, type RelayDatabase } from "../db.ts";13import * as RelayDb from "../db.ts";
14import { relayDpopProofs } from "../persistence/schema.ts";14import { relayDpopProofs } from "../persistence/schema.ts";
15import * as DpopProofs from "./DpopProofs.ts";15import * as DpopProofs from "./DpopProofs.ts";
16 16
@@ -78,8 +78,8 @@ function layer( · 61 unmodified lines
78 }),78 }),
79 };79 };
80 },80 },
81 } as unknown as RelayDatabase;81 } as unknown as RelayDb.RelayDb["Service"];
82 return DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)));82 return DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)));
83}83}
84 84
85function consumeEachProofOnce() {85function consumeEachProofOnce() {
infra/relay/src/auth/RelayTokens.test.ts+1−3
@@ -33,9 +33,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 32 unmodified lines
33 managedEndpointNamespace: undefined,33 managedEndpointNamespace: undefined,
34});34});
35 35
36const layer = RelayTokens.layer.pipe(36const layer = RelayTokens.layer.pipe(Layer.provide(RelayConfiguration.layer(config)));
37 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, config)),
38);
39 37
40describe("RelayTokens", () => {38describe("RelayTokens", () => {
41 it.effect("issues a user-bound environment link challenge", () =>39 it.effect("issues a user-bound environment link challenge", () =>
infra/relay/src/auth/RelayTokens.ts+36−37
@@ -93,45 +93,44 @@ function resolveDpopAccessTokenScopes(input: { · 92 unmodified lines
93 });93 });
94}94}
95 95
96export interface RelayTokensShape {96export class RelayTokens extends Context.Service<
97 readonly resolveDpopAccessTokenScopes: typeof resolveDpopAccessTokenScopes;97 RelayTokens,
98 readonly issueLinkChallenge: (input: {98 {
99 readonly userId: string;99 readonly resolveDpopAccessTokenScopes: typeof resolveDpopAccessTokenScopes;
100 readonly request: RelayEnvironmentLinkChallengeRequest;100 readonly issueLinkChallenge: (input: {
101 readonly jti: string;101 readonly userId: string;
102 readonly issuedAtEpochSeconds: number;102 readonly request: RelayEnvironmentLinkChallengeRequest;
103 readonly expiresAtEpochSeconds: number;103 readonly jti: string;
104 }) => Effect.Effect<string, RelayJwtError>;104 readonly issuedAtEpochSeconds: number;
105 readonly verifyLinkChallenge: (input: {105 readonly expiresAtEpochSeconds: number;
106 readonly token: string;106 }) => Effect.Effect<string, RelayJwtError>;
107 readonly userId: string;107 readonly verifyLinkChallenge: (input: {
108 readonly request: RelayEnvironmentLinkChallengeRequest;108 readonly token: string;
109 readonly nowEpochSeconds: number;109 readonly userId: string;
110 }) => Effect.Effect<LinkChallengeClaims | null>;110 readonly request: RelayEnvironmentLinkChallengeRequest;
111 readonly issueDpopAccessToken: (input: {111 readonly nowEpochSeconds: number;
112 readonly userId: string;112 }) => Effect.Effect<LinkChallengeClaims | null>;
113 readonly proofKeyThumbprint: string;113 readonly issueDpopAccessToken: (input: {
114 readonly jti: string;114 readonly userId: string;
115 readonly issuedAtEpochSeconds: number;115 readonly proofKeyThumbprint: string;
116 readonly expiresAtEpochSeconds: number;116 readonly jti: string;
117 readonly clientId: RelayPublicClientId;117 readonly issuedAtEpochSeconds: number;
118 readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>;118 readonly expiresAtEpochSeconds: number;
119 }) => Effect.Effect<string, RelayJwtError>;119 readonly clientId: RelayPublicClientId;
120 readonly verifyDpopAccessToken: (input: {120 readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>;
121 readonly token: string;121 }) => Effect.Effect<string, RelayJwtError>;
122 readonly nowEpochSeconds: number;122 readonly verifyDpopAccessToken: (input: {
123 }) => Effect.Effect<RelayDpopAccessTokenClaims | null>;123 readonly token: string;
124}124 readonly nowEpochSeconds: number;
125 125 }) => Effect.Effect<RelayDpopAccessTokenClaims | null>;
126export class RelayTokens extends Context.Service<RelayTokens, RelayTokensShape>()(126 }
127 "t3code-relay/auth/RelayTokens",127>()("t3code-relay/auth/RelayTokens") {}
128) {}
129 128
130const make = Effect.gen(function* () {129const make = Effect.gen(function* () {
131 const config = yield* RelayConfiguration.RelayConfiguration;130 const config = yield* RelayConfiguration.RelayConfiguration;
132 const issuer = normalizeRelayIssuer(config.relayIssuer);131 const issuer = normalizeRelayIssuer(config.relayIssuer);
133 132
134 const issueLinkChallenge: RelayTokensShape["issueLinkChallenge"] = Effect.fn(133 const issueLinkChallenge: RelayTokens["Service"]["issueLinkChallenge"] = Effect.fn(
135 "relay.tokens.issue_link_challenge",134 "relay.tokens.issue_link_challenge",
136 )(function* (input) {135 )(function* (input) {
137 return yield* signRelayJwt({136 return yield* signRelayJwt({
@@ -150,7 +149,7 @@ const make = Effect.gen(function* () { · 12 unmodified lines
150 });149 });
151 });150 });
152 151
153 const verifyLinkChallenge: RelayTokensShape["verifyLinkChallenge"] = Effect.fn(152 const verifyLinkChallenge: RelayTokens["Service"]["verifyLinkChallenge"] = Effect.fn(
154 "relay.tokens.verify_link_challenge",153 "relay.tokens.verify_link_challenge",
155 )((input) =>154 )((input) =>
156 verifyRelayJwt({155 verifyRelayJwt({
@@ -177,7 +176,7 @@ const make = Effect.gen(function* () { · 20 unmodified lines
177 ),176 ),
178 );177 );
179 178
180 const issueDpopAccessToken: RelayTokensShape["issueDpopAccessToken"] = Effect.fn(179 const issueDpopAccessToken: RelayTokens["Service"]["issueDpopAccessToken"] = Effect.fn(
181 "relay.tokens.issue_dpop_access_token",180 "relay.tokens.issue_dpop_access_token",
182 )(function* (input) {181 )(function* (input) {
183 return yield* signRelayJwt({182 return yield* signRelayJwt({
@@ -197,7 +196,7 @@ const make = Effect.gen(function* () { · 13 unmodified lines
197 });196 });
198 });197 });
199 198
200 const verifyDpopAccessToken: RelayTokensShape["verifyDpopAccessToken"] = Effect.fn(199 const verifyDpopAccessToken: RelayTokens["Service"]["verifyDpopAccessToken"] = Effect.fn(
201 "relay.tokens.verify_dpop_access_token",200 "relay.tokens.verify_dpop_access_token",
202 )((input) =>201 )((input) =>
203 verifyRelayJwt({202 verifyRelayJwt({
infra/relay/src/db.ts+6−5
@@ -10,11 +10,12 @@ import * as Effect from "effect/Effect"; · 9 unmodified lines
10 10
11import { relayDatabaseMode } from "./dbConfig.ts";11import { relayDatabaseMode } from "./dbConfig.ts";
12 12
13export interface RelayDatabase extends EffectPgDatabase {13export class RelayDb extends Context.Service<
14 readonly $client: PgClient;14 RelayDb,
15}15 EffectPgDatabase & {
16 16 readonly $client: PgClient;
17export class RelayDb extends Context.Service<RelayDb, RelayDatabase>()("t3code-relay/db/RelayDb") {}17 }
18>()("t3code-relay/db/RelayDb") {}
18 19
19export const PlanetscaleDatabase = Effect.gen(function* () {20export const PlanetscaleDatabase = Effect.gen(function* () {
20 const { stage } = yield* Alchemy.Stack;21 const { stage } = yield* Alchemy.Stack;
infra/relay/src/environments/EnvironmentConnector.test.ts+5−5
@@ -161,8 +161,8 @@ function connectorTestLayer( · 160 unmodified lines
161 request: HttpClientRequest.HttpClientRequest,161 request: HttpClientRequest.HttpClientRequest,
162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
163 options?: {163 options?: {
164 readonly links?: EnvironmentLinks.EnvironmentLinksShape;164 readonly links?: EnvironmentLinks.EnvironmentLinks["Service"];
165 readonly allocations?: ManagedEndpointAllocations.ManagedEndpointAllocationsShape;165 readonly allocations?: ManagedEndpointAllocations.ManagedEndpointAllocations["Service"];
166 },166 },
167) {167) {
168 return EnvironmentConnector.layer.pipe(168 return EnvironmentConnector.layer.pipe(
@@ -174,7 +174,7 @@ function connectorTestLayer( · 5 unmodified lines
174 options?.allocations ?? makeAllocations(),174 options?.allocations ?? makeAllocations(),
175 ),175 ),
176 ),176 ),
177 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, settings)),177 Layer.provide(RelayConfiguration.layer(settings)),
178 Layer.provide(Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute))),178 Layer.provide(Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute))),
179 );179 );
180}180}
@@ -189,7 +189,7 @@ function makeAllocations( · 8 unmodified lines
189 dnsRecordId: "dns-record-id",189 dnsRecordId: "dns-record-id",
190 readyAt: "2026-05-25T00:00:00.000Z",190 readyAt: "2026-05-25T00:00:00.000Z",
191 },191 },
192): ManagedEndpointAllocations.ManagedEndpointAllocationsShape {192): ManagedEndpointAllocations.ManagedEndpointAllocations["Service"] {
193 return {193 return {
194 get: () => Effect.succeed(allocation),194 get: () => Effect.succeed(allocation),
195 reserve: () => Effect.die("unused"),195 reserve: () => Effect.die("unused"),
@@ -202,7 +202,7 @@ function makeAllocations( · 6 unmodified lines
202 202
203function makeLinks(203function makeLinks(
204 overrides: Partial<EnvironmentLinks.RelayLinkedEnvironmentRecord> = {},204 overrides: Partial<EnvironmentLinks.RelayLinkedEnvironmentRecord> = {},
205): EnvironmentLinks.EnvironmentLinksShape {205): EnvironmentLinks.EnvironmentLinks["Service"] {
206 return {206 return {
207 upsert: () => Effect.void,207 upsert: () => Effect.void,
208 listUsersForEnvironment: () => Effect.succeed([]),208 listUsersForEnvironment: () => Effect.succeed([]),
infra/relay/src/environments/EnvironmentConnector.ts+15−15
@@ -35,7 +35,9 @@ import * as Layer from "effect/Layer"; · 34 unmodified lines
35import * as Option from "effect/Option";35import * as Option from "effect/Option";
36import * as Redacted from "effect/Redacted";36import * as Redacted from "effect/Redacted";
37import * as Schema from "effect/Schema";37import * as Schema from "effect/Schema";
38import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http";38import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
39import * as HttpClient from "effect/unstable/http/HttpClient";
40import * as HttpClientError from "effect/unstable/http/HttpClientError";
39 41
40import * as EnvironmentLinks from "./EnvironmentLinks.ts";42import * as EnvironmentLinks from "./EnvironmentLinks.ts";
41import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";43import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
@@ -139,22 +141,20 @@ export type EnvironmentConnectorError = · 97 unmodified lines
139export const ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS = 10_000;141export const ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS = 10_000;
140const ENVIRONMENT_HEALTH_CLOCK_SKEW_MILLIS = 60 * 1_000;142const ENVIRONMENT_HEALTH_CLOCK_SKEW_MILLIS = 60 * 1_000;
141 143
142export interface EnvironmentConnectorShape {
143 readonly connect: (input: {
144 readonly userId: string;
145 readonly environmentId: string;
146 readonly clientProofKeyThumbprint: string;
147 readonly deviceId?: string;
148 }) => Effect.Effect<RelayEnvironmentConnectResponse, EnvironmentConnectorError>;
149 readonly status: (input: {
150 readonly userId: string;
151 readonly environmentId: string;
152 }) => Effect.Effect<RelayEnvironmentStatusResponse, EnvironmentConnectorError>;
153}
154
155export class EnvironmentConnector extends Context.Service<144export class EnvironmentConnector extends Context.Service<
156 EnvironmentConnector,145 EnvironmentConnector,
157 EnvironmentConnectorShape146 {
147 readonly connect: (input: {
148 readonly userId: string;
149 readonly environmentId: string;
150 readonly clientProofKeyThumbprint: string;
151 readonly deviceId?: string;
152 }) => Effect.Effect<RelayEnvironmentConnectResponse, EnvironmentConnectorError>;
153 readonly status: (input: {
154 readonly userId: string;
155 readonly environmentId: string;
156 }) => Effect.Effect<RelayEnvironmentStatusResponse, EnvironmentConnectorError>;
157 }
158>()("t3code-relay/environments/EnvironmentConnector") {}158>()("t3code-relay/environments/EnvironmentConnector") {}
159 159
160const decodeMintResponseProof = Schema.decodeUnknownEffect(160const decodeMintResponseProof = Schema.decodeUnknownEffect(
infra/relay/src/environments/EnvironmentCredentials.test.ts+5−5
@@ -4,7 +4,7 @@ import { PgDialect, QueryBuilder } from "drizzle-orm/pg-core"; · 3 unmodified lines
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6 6
7import { RelayDb, type RelayDatabase } from "../db.ts";7import * as RelayDb from "../db.ts";
8import { relayEnvironmentCredentials } from "../persistence/schema.ts";8import { relayEnvironmentCredentials } from "../persistence/schema.ts";
9import * as EnvironmentCredentials from "./EnvironmentCredentials.ts";9import * as EnvironmentCredentials from "./EnvironmentCredentials.ts";
10 10
@@ -47,7 +47,7 @@ describe("EnvironmentCredentials", () => { · 36 unmodified lines
47 }),47 }),
48 };48 };
49 },49 },
50 } as unknown as RelayDatabase;50 } as unknown as RelayDb.RelayDb["Service"];
51 51
52 return Effect.gen(function* () {52 return Effect.gen(function* () {
53 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;53 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
@@ -87,7 +87,7 @@ describe("EnvironmentCredentials", () => { · 33 unmodified lines
87 Effect.provide(87 Effect.provide(
88 EnvironmentCredentials.layer.pipe(88 EnvironmentCredentials.layer.pipe(
89 Layer.provide(NodeCryptoLayer.layer),89 Layer.provide(NodeCryptoLayer.layer),
90 Layer.provide(Layer.succeed(RelayDb, fakeDb)),90 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
91 ),91 ),
92 ),92 ),
93 );93 );
@@ -118,7 +118,7 @@ describe("EnvironmentCredentials", () => { · 24 unmodified lines
118 },118 },
119 };119 };
120 },120 },
121 } as unknown as RelayDatabase;121 } as unknown as RelayDb.RelayDb["Service"];
122 122
123 return Effect.gen(function* () {123 return Effect.gen(function* () {
124 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;124 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
@@ -150,7 +150,7 @@ describe("EnvironmentCredentials", () => { · 25 unmodified lines
150 Effect.provide(150 Effect.provide(
151 EnvironmentCredentials.layer.pipe(151 EnvironmentCredentials.layer.pipe(
152 Layer.provide(NodeCryptoLayer.layer),152 Layer.provide(NodeCryptoLayer.layer),
153 Layer.provide(Layer.succeed(RelayDb, fakeDb)),153 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
154 ),154 ),
155 ),155 ),
156 );156 );
infra/relay/src/environments/EnvironmentCredentials.ts+18−20
@@ -8,7 +8,7 @@ import * as Option from "effect/Option"; · 7 unmodified lines
8import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
9import { and, eq, isNull, ne, notExists } from "drizzle-orm";9import { and, eq, isNull, ne, notExists } from "drizzle-orm";
10 10
11import { RelayDb } from "../db.ts";11import * as RelayDb from "../db.ts";
12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";
13 13
14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(
@@ -44,30 +44,28 @@ export interface EnvironmentCredentialPrincipal { · 29 unmodified lines
44 readonly environmentPublicKey: string;44 readonly environmentPublicKey: string;
45}45}
46 46
47export interface EnvironmentCredentialsShape {
48 readonly create: (input: {
49 readonly environmentId: string;
50 readonly environmentPublicKey: string;
51 }) => Effect.Effect<string, EnvironmentCredentialCreatePersistenceError>;
52 readonly authenticate: (
53 token: string,
54 ) => Effect.Effect<
55 Option.Option<EnvironmentCredentialPrincipal>,
56 EnvironmentCredentialAuthenticatePersistenceError
57 >;
58 readonly revokeForEnvironmentPublicKey: (input: {
59 readonly environmentId: string;
60 readonly environmentPublicKey: string;
61 }) => Effect.Effect<boolean, EnvironmentCredentialRevokePersistenceError>;
62}
63
64export class EnvironmentCredentials extends Context.Service<47export class EnvironmentCredentials extends Context.Service<
65 EnvironmentCredentials,48 EnvironmentCredentials,
66 EnvironmentCredentialsShape49 {
50 readonly create: (input: {
51 readonly environmentId: string;
52 readonly environmentPublicKey: string;
53 }) => Effect.Effect<string, EnvironmentCredentialCreatePersistenceError>;
54 readonly authenticate: (
55 token: string,
56 ) => Effect.Effect<
57 Option.Option<EnvironmentCredentialPrincipal>,
58 EnvironmentCredentialAuthenticatePersistenceError
59 >;
60 readonly revokeForEnvironmentPublicKey: (input: {
61 readonly environmentId: string;
62 readonly environmentPublicKey: string;
63 }) => Effect.Effect<boolean, EnvironmentCredentialRevokePersistenceError>;
64 }
67>()("t3code-relay/environments/EnvironmentCredentials") {}65>()("t3code-relay/environments/EnvironmentCredentials") {}
68 66
69const make = Effect.gen(function* () {67const make = Effect.gen(function* () {
70 const db = yield* RelayDb;68 const db = yield* RelayDb.RelayDb;
71 const crypto = yield* Crypto.Crypto;69 const crypto = yield* Crypto.Crypto;
72 const hashToken = (token: string) =>70 const hashToken = (token: string) =>
73 crypto71 crypto
infra/relay/src/environments/EnvironmentLinker.test.ts+3−3
@@ -105,14 +105,14 @@ const makeRequest = Effect.gen(function* () { · 104 unmodified lines
105});105});
106 106
107function testLayer(input?: {107function testLayer(input?: {
108 readonly upsert?: EnvironmentLinks.EnvironmentLinksShape["upsert"];108 readonly upsert?: EnvironmentLinks.EnvironmentLinks["Service"]["upsert"];
109 readonly consume?: DpopProofs.DpopProofReplayShape["consume"];109 readonly consume?: DpopProofs.DpopProofReplay["Service"]["consume"];
110}) {110}) {
111 return EnvironmentLinker.layer.pipe(111 return EnvironmentLinker.layer.pipe(
112 Layer.provideMerge(RelayTokens.layer),112 Layer.provideMerge(RelayTokens.layer),
113 Layer.provide(113 Layer.provide(
114 Layer.mergeAll(114 Layer.mergeAll(
115 Layer.succeed(RelayConfiguration.RelayConfiguration, config),115 RelayConfiguration.layer(config),
116 Layer.succeed(DpopProofs.DpopProofReplay, {116 Layer.succeed(DpopProofs.DpopProofReplay, {
117 verifyAndConsume: () => Effect.die("unexpected DPoP proof verification"),117 verifyAndConsume: () => Effect.die("unexpected DPoP proof verification"),
118 consume: input?.consume ?? (() => Effect.succeed(true)),118 consume: input?.consume ?? (() => Effect.succeed(true)),
infra/relay/src/environments/EnvironmentLinker.ts+19−20
@@ -53,26 +53,25 @@ export type EnvironmentLinkError = · 52 unmodified lines
53 | EnvironmentCredentials.EnvironmentCredentialCreatePersistenceError53 | EnvironmentCredentials.EnvironmentCredentialCreatePersistenceError
54 | ManagedEndpointProvider.ManagedEndpointProviderError;54 | ManagedEndpointProvider.ManagedEndpointProviderError;
55 55
56export interface EnvironmentLinkerShape {56export class EnvironmentLinker extends Context.Service<
57 readonly link: (input: {57 EnvironmentLinker,
58 readonly userId: string;58 {
59 readonly request: RelayEnvironmentLinkRequest;59 readonly link: (input: {
60 }) => Effect.Effect<60 readonly userId: string;
61 {61 readonly request: RelayEnvironmentLinkRequest;
62 readonly environmentId: RelayEnvironmentLinkProofPayload["environmentId"];62 }) => Effect.Effect<
63 readonly endpoint: RelayEnvironmentLinkProofPayload["endpoint"];63 {
64 readonly endpointRuntime:64 readonly environmentId: RelayEnvironmentLinkProofPayload["environmentId"];
65 | ManagedEndpointProvider.ManagedEndpointProvisioningResult["runtime"]65 readonly endpoint: RelayEnvironmentLinkProofPayload["endpoint"];
66 | null;66 readonly endpointRuntime:
67 readonly environmentCredential: string;67 | ManagedEndpointProvider.ManagedEndpointProvisioningResult["runtime"]
68 },68 | null;
69 EnvironmentLinkError69 readonly environmentCredential: string;
70 >;70 },
71}71 EnvironmentLinkError
72 72 >;
73export class EnvironmentLinker extends Context.Service<EnvironmentLinker, EnvironmentLinkerShape>()(73 }
74 "t3code-relay/environments/EnvironmentLinker",74>()("t3code-relay/environments/EnvironmentLinker") {}
75) {}
76 75
77const decodeProof = Schema.decodeUnknownEffect(RelayEnvironmentLinkProofPayload);76const decodeProof = Schema.decodeUnknownEffect(RelayEnvironmentLinkProofPayload);
78 77
infra/relay/src/environments/EnvironmentLinks.test.ts+16−8
@@ -3,9 +3,9 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import { PgDialect } from "drizzle-orm/pg-core";4import { PgDialect } from "drizzle-orm/pg-core";
5 5
6import { RelayDb, type RelayDatabase } from "../db.ts";6import * as RelayDb from "../db.ts";
7import { relayEnvironmentLinks } from "../persistence/schema.ts";7import { relayEnvironmentLinks } from "../persistence/schema.ts";
8import { EnvironmentLinks, layer } from "./EnvironmentLinks.ts";8import * as EnvironmentLinks from "./EnvironmentLinks.ts";
9 9
10describe("EnvironmentLinks", () => {10describe("EnvironmentLinks", () => {
11 it.effect("selects users when either notifications or Live Activities are enabled", () => {11 it.effect("selects users when either notifications or Live Activities are enabled", () => {
@@ -25,10 +25,10 @@ describe("EnvironmentLinks", () => { · 13 unmodified lines
25 },25 },
26 };26 };
27 },27 },
28 } as unknown as RelayDatabase;28 } as unknown as RelayDb.RelayDb["Service"];
29 29
30 return Effect.gen(function* () {30 return Effect.gen(function* () {
31 const links = yield* EnvironmentLinks;31 const links = yield* EnvironmentLinks.EnvironmentLinks;
32 expect(yield* links.listUsersForEnvironment({ environmentId: "env-1" })).toEqual([]);32 expect(yield* links.listUsersForEnvironment({ environmentId: "env-1" })).toEqual([]);
33 expect(whereConditions).toHaveLength(1);33 expect(whereConditions).toHaveLength(1);
34 34
@@ -39,7 +39,11 @@ describe("EnvironmentLinks", () => { · 4 unmodified lines
39 expect(query.sql).toContain('"relay_environment_links"."live_activities_enabled" = $3');39 expect(query.sql).toContain('"relay_environment_links"."live_activities_enabled" = $3');
40 expect(query.sql).toContain(" or ");40 expect(query.sql).toContain(" or ");
41 expect(query.params).toEqual(["env-1", true, true]);41 expect(query.params).toEqual(["env-1", true, true]);
42 }).pipe(Effect.provide(layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));42 }).pipe(
43 Effect.provide(
44 EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
45 ),
46 );
43 });47 });
44 48
45 it.effect("revokes only the active link owned by the requesting user", () => {49 it.effect("revokes only the active link owned by the requesting user", () => {
@@ -65,10 +69,10 @@ describe("EnvironmentLinks", () => { · 19 unmodified lines
65 },69 },
66 };70 };
67 },71 },
68 } as unknown as RelayDatabase;72 } as unknown as RelayDb.RelayDb["Service"];
69 73
70 return Effect.gen(function* () {74 return Effect.gen(function* () {
71 const links = yield* EnvironmentLinks;75 const links = yield* EnvironmentLinks.EnvironmentLinks;
72 const revoked = yield* links.revokeForUser({76 const revoked = yield* links.revokeForUser({
73 userId: "user-1",77 userId: "user-1",
74 environmentId: "env-1",78 environmentId: "env-1",
@@ -86,6 +90,10 @@ describe("EnvironmentLinks", () => { · 11 unmodified lines
86 expect(query.sql).toContain('"relay_environment_links"."environment_id" = $2');90 expect(query.sql).toContain('"relay_environment_links"."environment_id" = $2');
87 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');91 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');
88 expect(query.params).toEqual(["user-1", "env-1"]);92 expect(query.params).toEqual(["user-1", "env-1"]);
89 }).pipe(Effect.provide(layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));93 }).pipe(
94 Effect.provide(
95 EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
96 ),
97 );
90 });98 });
91});99});
infra/relay/src/environments/EnvironmentLinks.ts+40−41
@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer"; · 10 unmodified lines
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
12import { and, eq, isNull, or } from "drizzle-orm";12import { and, eq, isNull, or } from "drizzle-orm";
13 13
14import { RelayDb } from "../db.ts";14import * as RelayDb from "../db.ts";
15import { relayEnvironmentLinks } from "../persistence/schema.ts";15import { relayEnvironmentLinks } from "../persistence/schema.ts";
16 16
17export interface RelayLinkedEnvironmentRecord extends RelayClientEnvironmentRecord {17export interface RelayLinkedEnvironmentRecord extends RelayClientEnvironmentRecord {
@@ -78,45 +78,44 @@ export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorCla · 60 unmodified lines
78 }78 }
79}79}
80 80
81export interface EnvironmentLinksShape {81export class EnvironmentLinks extends Context.Service<
82 readonly upsert: (input: {82 EnvironmentLinks,
83 readonly userId: string;83 {
84 readonly request: RelayEnvironmentLinkRequest;84 readonly upsert: (input: {
85 readonly proof: RelayEnvironmentLinkProofPayload;85 readonly userId: string;
86 readonly endpoint: RelayManagedEndpoint;86 readonly request: RelayEnvironmentLinkRequest;
87 }) => Effect.Effect<void, EnvironmentLinkUpsertPersistenceError>;87 readonly proof: RelayEnvironmentLinkProofPayload;
88 readonly listUsersForEnvironment: (input: {88 readonly endpoint: RelayManagedEndpoint;
89 readonly environmentId: string;89 }) => Effect.Effect<void, EnvironmentLinkUpsertPersistenceError>;
90 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentLinkUserListPersistenceError>;90 readonly listUsersForEnvironment: (input: {
91 readonly listDeliveryUsersForEnvironment: (input: {91 readonly environmentId: string;
92 readonly environmentId: string;92 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentLinkUserListPersistenceError>;
93 readonly environmentPublicKey: string;93 readonly listDeliveryUsersForEnvironment: (input: {
94 }) => Effect.Effect<94 readonly environmentId: string;
95 ReadonlyArray<AgentAwarenessDeliveryUserRecord>,95 readonly environmentPublicKey: string;
96 EnvironmentLinkUserListPersistenceError96 }) => Effect.Effect<
97 >;97 ReadonlyArray<AgentAwarenessDeliveryUserRecord>,
98 readonly listPublicKeysForEnvironment: (input: {98 EnvironmentLinkUserListPersistenceError
99 readonly environmentId: string;99 >;
100 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentPublicKeyListPersistenceError>;100 readonly listPublicKeysForEnvironment: (input: {
101 readonly listForUser: (input: {101 readonly environmentId: string;
102 readonly userId: string;102 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentPublicKeyListPersistenceError>;
103 }) => Effect.Effect<103 readonly listForUser: (input: {
104 ReadonlyArray<RelayClientEnvironmentRecord>,104 readonly userId: string;
105 EnvironmentLinkListPersistenceError105 }) => Effect.Effect<
106 >;106 ReadonlyArray<RelayClientEnvironmentRecord>,
107 readonly getForUser: (input: {107 EnvironmentLinkListPersistenceError
108 readonly userId: string;108 >;
109 readonly environmentId: string;109 readonly getForUser: (input: {
110 }) => Effect.Effect<RelayLinkedEnvironmentRecord | null, EnvironmentLinkLookupPersistenceError>;110 readonly userId: string;
111 readonly revokeForUser: (input: {111 readonly environmentId: string;
112 readonly userId: string;112 }) => Effect.Effect<RelayLinkedEnvironmentRecord | null, EnvironmentLinkLookupPersistenceError>;
113 readonly environmentId: string;113 readonly revokeForUser: (input: {
114 }) => Effect.Effect<boolean, EnvironmentLinkRevokePersistenceError>;114 readonly userId: string;
115}115 readonly environmentId: string;
116 116 }) => Effect.Effect<boolean, EnvironmentLinkRevokePersistenceError>;
117export class EnvironmentLinks extends Context.Service<EnvironmentLinks, EnvironmentLinksShape>()(117 }
118 "t3code-relay/environments/EnvironmentLinks",118>()("t3code-relay/environments/EnvironmentLinks") {}
119) {}
120 119
121function agentAwarenessDeliveryUserCondition(environmentId: string) {120function agentAwarenessDeliveryUserCondition(environmentId: string) {
122 return and(121 return and(
@@ -140,7 +139,7 @@ function agentAwarenessDeliveryUserKeyCondition(input: { · 17 unmodified lines
140}139}
141 140
142const make = Effect.gen(function* () {141const make = Effect.gen(function* () {
143 const db = yield* RelayDb;142 const db = yield* RelayDb.RelayDb;
144 143
145 return EnvironmentLinks.of({144 return EnvironmentLinks.of({
146 upsert: Effect.fn("relay.environment_links.upsert")(145 upsert: Effect.fn("relay.environment_links.upsert")(
infra/relay/src/environments/EnvironmentPublishSignatures.test.ts+2−2
@@ -80,11 +80,11 @@ const freshRequest = Effect.gen(function* () { · 79 unmodified lines
80 } satisfies RelayAgentActivityPublishRequest;80 } satisfies RelayAgentActivityPublishRequest;
81});81});
82 82
83function layer(replay?: Partial<DpopProofs.DpopProofReplayShape>) {83function layer(replay?: Partial<DpopProofs.DpopProofReplay["Service"]>) {
84 return EnvironmentPublishSignatures.layer.pipe(84 return EnvironmentPublishSignatures.layer.pipe(
85 Layer.provide(85 Layer.provide(
86 Layer.merge(86 Layer.merge(
87 Layer.succeed(RelayConfiguration.RelayConfiguration, config),87 RelayConfiguration.layer(config),
88 Layer.succeed(DpopProofs.DpopProofReplay, {88 Layer.succeed(DpopProofs.DpopProofReplay, {
89 verifyAndConsume:89 verifyAndConsume:
90 replay?.verifyAndConsume ?? (() => Effect.die("unexpected DPoP proof verification")),90 replay?.verifyAndConsume ?? (() => Effect.die("unexpected DPoP proof verification")),
infra/relay/src/environments/EnvironmentPublishSignatures.ts+8−10
@@ -59,18 +59,16 @@ export type EnvironmentPublishSignatureError = · 58 unmodified lines
59 | EnvironmentPublishPublicKeyMissing59 | EnvironmentPublishPublicKeyMissing
60 | DpopProofs.DpopProofReplayPersistenceError;60 | DpopProofs.DpopProofReplayPersistenceError;
61 61
62export interface EnvironmentPublishSignaturesShape {
63 readonly verify: (input: {
64 readonly environmentId: string;
65 readonly environmentPublicKey: string;
66 readonly threadId: string;
67 readonly request: RelayAgentActivityPublishRequest;
68 }) => Effect.Effect<void, EnvironmentPublishSignatureError>;
69}
70
71export class EnvironmentPublishSignatures extends Context.Service<62export class EnvironmentPublishSignatures extends Context.Service<
72 EnvironmentPublishSignatures,63 EnvironmentPublishSignatures,
73 EnvironmentPublishSignaturesShape64 {
65 readonly verify: (input: {
66 readonly environmentId: string;
67 readonly environmentPublicKey: string;
68 readonly threadId: string;
69 readonly request: RelayAgentActivityPublishRequest;
70 }) => Effect.Effect<void, EnvironmentPublishSignatureError>;
71 }
74>()("t3code-relay/environments/EnvironmentPublishSignatures") {}72>()("t3code-relay/environments/EnvironmentPublishSignatures") {}
75 73
76const decodeProof = Schema.decodeUnknownEffect(RelayAgentActivityPublishProofPayload);74const decodeProof = Schema.decodeUnknownEffect(RelayAgentActivityPublishProofPayload);
infra/relay/src/environments/ManagedEndpointAllocations.ts+26−28
@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect"; · 5 unmodified lines
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
8 8
9import { RelayDb } from "../db.ts";9import * as RelayDb from "../db.ts";
10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";
11import { relayManagedEndpointAllocations } from "../persistence/schema.ts";11import { relayManagedEndpointAllocations } from "../persistence/schema.ts";
12 12
@@ -66,26 +66,29 @@ interface RecordManagedEndpointDnsInput extends ManagedEndpointAllocationKey { · 53 unmodified lines
66 readonly dnsRecordId: string;66 readonly dnsRecordId: string;
67}67}
68 68
69export interface ManagedEndpointAllocationsShape {69export class ManagedEndpointAllocations extends Context.Service<
70 readonly get: (70 ManagedEndpointAllocations,
71 input: ManagedEndpointAllocationKey,71 {
72 ) => Effect.Effect<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>;72 readonly get: (
73 readonly reserve: (73 input: ManagedEndpointAllocationKey,
74 input: ReserveManagedEndpointAllocationInput,74 ) => Effect.Effect<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>;
75 ) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>;75 readonly reserve: (
76 readonly recordTunnel: (76 input: ReserveManagedEndpointAllocationInput,
77 input: RecordManagedEndpointTunnelInput,77 ) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>;
78 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;78 readonly recordTunnel: (
79 readonly recordDns: (79 input: RecordManagedEndpointTunnelInput,
80 input: RecordManagedEndpointDnsInput,80 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
81 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;81 readonly recordDns: (
82 readonly markReady: (82 input: RecordManagedEndpointDnsInput,
83 input: ManagedEndpointAllocationKey,83 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
84 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;84 readonly markReady: (
85 readonly remove: (85 input: ManagedEndpointAllocationKey,
86 input: ManagedEndpointAllocationKey,86 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
87 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;87 readonly remove: (
88}88 input: ManagedEndpointAllocationKey,
89 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
90 }
91>()("t3code-relay/environments/ManagedEndpointAllocations") {}
89 92
90const allocationSelection = {93const allocationSelection = {
91 userId: relayManagedEndpointAllocations.userId,94 userId: relayManagedEndpointAllocations.userId,
@@ -109,7 +112,7 @@ const persistenceError = (cause: unknown) => · 17 unmodified lines
109 : new ManagedEndpointAllocationPersistenceError({ cause });112 : new ManagedEndpointAllocationPersistenceError({ cause });
110 113
111const make = Effect.gen(function* () {114const make = Effect.gen(function* () {
112 const db = yield* RelayDb;115 const db = yield* RelayDb.RelayDb;
113 116
114 return ManagedEndpointAllocations.of({117 return ManagedEndpointAllocations.of({
115 get: Effect.fn("relay.managed_endpoint_allocations.get")(function* (118 get: Effect.fn("relay.managed_endpoint_allocations.get")(function* (
@@ -198,9 +201,4 @@ const make = Effect.gen(function* () { · 82 unmodified lines
198 });201 });
199});202});
200 203
201export class ManagedEndpointAllocations extends Context.Service<204export const layer = Layer.effect(ManagedEndpointAllocations, make);
202 ManagedEndpointAllocations,
203 ManagedEndpointAllocationsShape
204>()("t3code-relay/environments/ManagedEndpointAllocations") {
205 static readonly layer = Layer.effect(this, make);
206}
infra/relay/src/environments/ManagedEndpointProvider.test.ts+3−3
@@ -204,9 +204,9 @@ function providerLayer( · 203 unmodified lines
204) {204) {
205 return ManagedEndpointProvider.layer.pipe(205 return ManagedEndpointProvider.layer.pipe(
206 Layer.provideMerge(NodeServices.layer),206 Layer.provideMerge(NodeServices.layer),
207 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, config)),207 Layer.provide(RelayConfiguration.layer(config)),
208 Layer.provide(Layer.succeed(ManagedEndpointProvider.ManagedEndpointTunnelClient, tunnelClient)),208 Layer.provide(ManagedEndpointProvider.layerTunnelClient(tunnelClient)),
209 Layer.provide(Layer.succeed(ManagedEndpointProvider.ManagedEndpointDnsClient, dnsClient)),209 Layer.provide(ManagedEndpointProvider.layerDnsClient(dnsClient)),
210 Layer.provide(210 Layer.provide(
211 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, allocations),211 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, allocations),
212 ),212 ),
infra/relay/src/environments/ManagedEndpointProvider.ts+69−63
@@ -23,7 +23,7 @@ import { · 22 unmodified lines
23 managedEndpointHostname,23 managedEndpointHostname,
24 managedEndpointTunnelName,24 managedEndpointTunnelName,
25} from "../deploymentConfig.ts";25} from "../deploymentConfig.ts";
26import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts";26import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
27 27
28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(
29 "ManagedEndpointProvisioningNotConfigured",29 "ManagedEndpointProvisioningNotConfigured",
@@ -74,21 +74,19 @@ export interface ManagedEndpointProvisioningResult { · 44 unmodified lines
74 readonly runtime: RelayManagedEndpointRuntimeConfig;74 readonly runtime: RelayManagedEndpointRuntimeConfig;
75}75}
76 76
77export interface ManagedEndpointProviderShape {
78 readonly provision: (input: {
79 readonly userId: string;
80 readonly environmentId: string;
81 readonly origin: RelayManagedEndpointOrigin;
82 }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>;
83 readonly deprovision: (input: {
84 readonly userId: string;
85 readonly environmentId: string;
86 }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>;
87}
88
89export class ManagedEndpointProvider extends Context.Service<77export class ManagedEndpointProvider extends Context.Service<
90 ManagedEndpointProvider,78 ManagedEndpointProvider,
91 ManagedEndpointProviderShape79 {
80 readonly provision: (input: {
81 readonly userId: string;
82 readonly environmentId: string;
83 readonly origin: RelayManagedEndpointOrigin;
84 }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>;
85 readonly deprovision: (input: {
86 readonly userId: string;
87 readonly environmentId: string;
88 }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>;
89 }
92>()("t3code-relay/environments/ManagedEndpointProvider") {}90>()("t3code-relay/environments/ManagedEndpointProvider") {}
93 91
94interface ManagedEndpointTunnel {92interface ManagedEndpointTunnel {
@@ -105,36 +103,42 @@ export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<Ma · 10 unmodified lines
105 }103 }
106}104}
107 105
108export interface ManagedEndpointTunnelClientShape {
109 readonly list: (request: {
110 readonly name: string;
111 readonly isDeleted: false;
112 }) => Effect.Effect<
113 { readonly result: ReadonlyArray<ManagedEndpointTunnel> },
114 ManagedEndpointTunnelClientError
115 >;
116 readonly create: (request: {
117 readonly name: string;
118 readonly configSrc: "cloudflare";
119 }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>;
120 readonly putConfiguration: (
121 tunnelId: string,
122 config: {
123 readonly ingress: Array<{
124 readonly hostname?: string;
125 readonly service: string;
126 }>;
127 },
128 ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
129 readonly getToken: (tunnelId: string) => Effect.Effect<string, ManagedEndpointTunnelClientError>;
130 readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
131}
132
133export class ManagedEndpointTunnelClient extends Context.Service<106export class ManagedEndpointTunnelClient extends Context.Service<
134 ManagedEndpointTunnelClient,107 ManagedEndpointTunnelClient,
135 ManagedEndpointTunnelClientShape108 {
109 readonly list: (request: {
110 readonly name: string;
111 readonly isDeleted: false;
112 }) => Effect.Effect<
113 { readonly result: ReadonlyArray<ManagedEndpointTunnel> },
114 ManagedEndpointTunnelClientError
115 >;
116 readonly create: (request: {
117 readonly name: string;
118 readonly configSrc: "cloudflare";
119 }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>;
120 readonly putConfiguration: (
121 tunnelId: string,
122 config: {
123 readonly ingress: Array<{
124 readonly hostname?: string;
125 readonly service: string;
126 }>;
127 },
128 ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
129 readonly getToken: (
130 tunnelId: string,
131 ) => Effect.Effect<string, ManagedEndpointTunnelClientError>;
132 readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
133 }
136>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {}134>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {}
137 135
136export const makeTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
137 ManagedEndpointTunnelClient.of(client);
138
139export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
140 Layer.succeed(ManagedEndpointTunnelClient, makeTunnelClient(client));
141
138interface ManagedEndpointCnameRecordInput {142interface ManagedEndpointCnameRecordInput {
139 readonly type: "CNAME";143 readonly type: "CNAME";
140 readonly name: string;144 readonly name: string;
@@ -152,29 +156,33 @@ export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<Manag · 11 unmodified lines
152 }156 }
153}157}
154 158
155export interface ManagedEndpointDnsClientShape {
156 readonly listRecords: (
157 hostname: string,
158 ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>;
159 readonly createRecord: (
160 request: ManagedEndpointCnameRecordInput,
161 ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>;
162 readonly updateRecord: (
163 dnsRecordId: string,
164 request: ManagedEndpointCnameRecordInput,
165 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
166 readonly deleteRecord: (
167 dnsRecordId: string,
168 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
169}
170
171export class ManagedEndpointDnsClient extends Context.Service<159export class ManagedEndpointDnsClient extends Context.Service<
172 ManagedEndpointDnsClient,160 ManagedEndpointDnsClient,
173 ManagedEndpointDnsClientShape161 {
162 readonly listRecords: (
163 hostname: string,
164 ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>;
165 readonly createRecord: (
166 request: ManagedEndpointCnameRecordInput,
167 ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>;
168 readonly updateRecord: (
169 dnsRecordId: string,
170 request: ManagedEndpointCnameRecordInput,
171 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
172 readonly deleteRecord: (
173 dnsRecordId: string,
174 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
175 }
174>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {}176>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {}
175 177
178export const makeDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
179 ManagedEndpointDnsClient.of(client);
180
181export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
182 Layer.succeed(ManagedEndpointDnsClient, makeDnsClient(client));
183
176const requireCloudflareSettings = Effect.fnUntraced(function* (184const requireCloudflareSettings = Effect.fnUntraced(function* (
177 settings: RelayConfiguration.RelayConfigurationShape,185 settings: RelayConfiguration.RelayConfiguration["Service"],
178) {186) {
179 if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) {187 if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) {
180 return yield* new ManagedEndpointProvisioningNotConfigured();188 return yield* new ManagedEndpointProvisioningNotConfigured();
@@ -234,7 +242,7 @@ const make = Effect.gen(function* () { · 53 unmodified lines
234 const crypto = yield* Crypto.Crypto;242 const crypto = yield* Crypto.Crypto;
235 const tunnels = yield* ManagedEndpointTunnelClient;243 const tunnels = yield* ManagedEndpointTunnelClient;
236 const dns = yield* ManagedEndpointDnsClient;244 const dns = yield* ManagedEndpointDnsClient;
237 const allocations = yield* ManagedEndpointAllocations;245 const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations;
238 246
239 const updateExistingDnsRecords = Effect.fnUntraced(function* (247 const updateExistingDnsRecords = Effect.fnUntraced(function* (
240 records: ReadonlyArray<{ readonly id: string }>,248 records: ReadonlyArray<{ readonly id: string }>,
@@ -452,8 +460,7 @@ export const layerCloudflareBindings = ( · 211 unmodified lines
452 layer.pipe(460 layer.pipe(
453 Layer.provide(461 Layer.provide(
454 Layer.mergeAll(462 Layer.mergeAll(
455 Layer.succeed(463 layerTunnelClient(
456 ManagedEndpointTunnelClient,
457 ManagedEndpointTunnelClient.of({464 ManagedEndpointTunnelClient.of({
458 list: (request) =>465 list: (request) =>
459 tunnelClient.list(request).pipe(466 tunnelClient.list(request).pipe(
@@ -482,8 +489,7 @@ export const layerCloudflareBindings = ( · 22 unmodified lines
482 ),489 ),
483 }),490 }),
484 ),491 ),
485 Layer.succeed(492 layerDnsClient(
486 ManagedEndpointDnsClient,
487 ManagedEndpointDnsClient.of({493 ManagedEndpointDnsClient.of({
488 listRecords: (hostname) =>494 listRecords: (hostname) =>
489 dnsClient.listDnsRecords({ search: hostname }).pipe(495 dnsClient.listDnsRecords({ search: hostname }).pipe(
infra/relay/src/http/Api.test.ts+2−2
@@ -30,7 +30,7 @@ vi.mock("@clerk/backend", () => ({ · 29 unmodified lines
30 verifyToken: vi.fn(),30 verifyToken: vi.fn(),
31}));31}));
32 32
33const relaySettings: RelayConfiguration.RelayConfigurationShape = {33const relaySettings: RelayConfiguration.RelayConfiguration["Service"] = {
34 relayIssuer: "https://relay.example.test",34 relayIssuer: "https://relay.example.test",
35 apns: {35 apns: {
36 teamId: "apns-team",36 teamId: "apns-team",
@@ -110,7 +110,7 @@ describe("relay environment authentication", () => { · 73 unmodified lines
110 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({110 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({
111 cause: "database unavailable",111 cause: "database unavailable",
112 });112 });
113 const credentials: EnvironmentCredentials.EnvironmentCredentialsShape = {113 const credentials: EnvironmentCredentials.EnvironmentCredentials["Service"] = {
114 create: () => Effect.die("unused create"),114 create: () => Effect.die("unused create"),
115 authenticate: () => Effect.fail(failure),115 authenticate: () => Effect.fail(failure),
116 revokeForEnvironmentPublicKey: () => Effect.die("unused revoke"),116 revokeForEnvironmentPublicKey: () => Effect.die("unused revoke"),
infra/relay/src/http/Api.ts+8−5
@@ -66,7 +66,7 @@ import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllo · 65 unmodified lines
66import * as EnvironmentPublishSignatures from "../environments/EnvironmentPublishSignatures.ts";66import * as EnvironmentPublishSignatures from "../environments/EnvironmentPublishSignatures.ts";
67import * as MobileRegistrations from "../agentActivity/MobileRegistrations.ts";67import * as MobileRegistrations from "../agentActivity/MobileRegistrations.ts";
68import { withSpanAttributes } from "../observability.ts";68import { withSpanAttributes } from "../observability.ts";
69import { RelayDb } from "../db.ts";69import * as RelayDb from "../db.ts";
70 70
71const relayCorsAllowedMethods = ["GET", "POST", "DELETE", "OPTIONS"] as const;71const relayCorsAllowedMethods = ["GET", "POST", "DELETE", "OPTIONS"] as const;
72const relayCorsAllowedHeaders = [72const relayCorsAllowedHeaders = [
@@ -347,7 +347,7 @@ export const healthApi = HttpApiBuilder.group( · 274 unmodified lines
347 RelayApi,347 RelayApi,
348 "health",348 "health",
349 Effect.fnUntraced(function* (handlers) {349 Effect.fnUntraced(function* (handlers) {
350 const db = yield* RelayDb;350 const db = yield* RelayDb.RelayDb;
351 return handlers.handle(351 return handlers.handle(
352 "health",352 "health",
353 Effect.fn("relay.api.health")(353 Effect.fn("relay.api.health")(
@@ -985,7 +985,10 @@ function hasExpectedClerkAudience(audience: unknown, expectedAudience: string): · 631 unmodified lines
985 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);985 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);
986}986}
987 987
988function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationShape, token: string) {988function verifyClerkBearerToken(
989 config: RelayConfiguration.RelayConfiguration["Service"],
990 token: string,
991) {
989 return Effect.tryPromise({992 return Effect.tryPromise({
990 try: () =>993 try: () =>
991 verifyToken(token, {994 verifyToken(token, {
@@ -1001,7 +1004,7 @@ function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationSha · 9 unmodified lines
1001}1004}
1002 1005
1003function verifyClerkOAuthBearerToken(1006function verifyClerkOAuthBearerToken(
1004 config: RelayConfiguration.RelayConfigurationShape,1007 config: RelayConfiguration.RelayConfiguration["Service"],
1005 token: string,1008 token: string,
1006) {1009) {
1007 return Effect.tryPromise({1010 return Effect.tryPromise({
@@ -1027,7 +1030,7 @@ function verifyClerkOAuthBearerToken( · 19 unmodified lines
1027}1030}
1028 1031
1029export function verifyRelayClientBearerToken(1032export function verifyRelayClientBearerToken(
1030 config: RelayConfiguration.RelayConfigurationShape,1033 config: RelayConfiguration.RelayConfiguration["Service"],
1031 token: string,1034 token: string,
1032) {1035) {
1033 return verifyClerkBearerToken(config, token).pipe(1036 return verifyClerkBearerToken(config, token).pipe(
infra/relay/src/observability.test.ts+2−2
@@ -9,7 +9,7 @@ import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; · 8 unmodified lines
9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
10import type { OtlpTracer } from "effect/unstable/observability";10import type { OtlpTracer } from "effect/unstable/observability";
11 11
12import { EnvironmentConnectNotAuthorized } from "./environments/EnvironmentConnector.ts";12import * as EnvironmentConnector from "./environments/EnvironmentConnector.ts";
13import { makeRelayTraceLayer } from "./observability.ts";13import { makeRelayTraceLayer } from "./observability.ts";
14 14
15interface ExportedRequest {15interface ExportedRequest {
@@ -43,7 +43,7 @@ it.effect("exports schema error fields as span attributes", () => · 27 unmodified lines
43 );43 );
44 44
45 yield* Effect.fail(45 yield* Effect.fail(
46 new EnvironmentConnectNotAuthorized({46 new EnvironmentConnector.EnvironmentConnectNotAuthorized({
47 environmentId: "environment-1",47 environmentId: "environment-1",
48 operation: "connect",48 operation: "connect",
49 reason: "managed_endpoint_allocation_not_ready",49 reason: "managed_endpoint_allocation_not_ready",
infra/relay/src/worker.ts+4−9
@@ -42,7 +42,7 @@ import * as EnvironmentCredentials from "./environments/EnvironmentCredentials.t · 41 unmodified lines
42import * as EnvironmentLinks from "./environments/EnvironmentLinks.ts";42import * as EnvironmentLinks from "./environments/EnvironmentLinks.ts";
43import * as ManagedEndpointAllocations from "./environments/ManagedEndpointAllocations.ts";43import * as ManagedEndpointAllocations from "./environments/ManagedEndpointAllocations.ts";
44import * as LiveActivities from "./agentActivity/LiveActivities.ts";44import * as LiveActivities from "./agentActivity/LiveActivities.ts";
45import { RelayDb, RelayHyperdrive } from "./db.ts";45import * as RelayDb from "./db.ts";
46import { RelayApnsDeliveryDeadLetterQueue, RelayApnsDeliveryQueue } from "./queues.ts";46import { RelayApnsDeliveryDeadLetterQueue, RelayApnsDeliveryQueue } from "./queues.ts";
47import * as RelayConfiguration from "./Config.ts";47import * as RelayConfiguration from "./Config.ts";
48import * as AgentActivityPublisher from "./agentActivity/AgentActivityPublisher.ts";48import * as AgentActivityPublisher from "./agentActivity/AgentActivityPublisher.ts";
@@ -138,7 +138,7 @@ export default class Api extends Cloudflare.Worker<Api>()( · 89 unmodified lines
138 138
139 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;139 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;
140 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;140 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;
141 const hyperdrive = yield* Cloudflare.Hyperdrive.bind(yield* RelayHyperdrive);141 const hyperdrive = yield* Cloudflare.Hyperdrive.bind(yield* RelayDb.RelayHyperdrive);
142 const db = yield* Drizzle.postgres(hyperdrive.connectionString);142 const db = yield* Drizzle.postgres(hyperdrive.connectionString);
143 143
144 const managedEndpointTunnelBinding = yield* Cloudflare.TunnelReadWrite.bind();144 const managedEndpointTunnelBinding = yield* Cloudflare.TunnelReadWrite.bind();
@@ -203,16 +203,11 @@ export default class Api extends Cloudflare.Worker<Api>()( · 58 unmodified lines
203 Layer.provideMerge(AgentActivityRows.layer),203 Layer.provideMerge(AgentActivityRows.layer),
204 Layer.provideMerge(Devices.layer),204 Layer.provideMerge(Devices.layer),
205 Layer.provideMerge(EnvironmentCredentials.layer),205 Layer.provideMerge(EnvironmentCredentials.layer),
206 Layer.provideMerge(206 Layer.provideMerge(Layer.mergeAll(EnvironmentLinks.layer, ManagedEndpointAllocations.layer)),
207 Layer.mergeAll(
208 EnvironmentLinks.layer,
209 ManagedEndpointAllocations.ManagedEndpointAllocations.layer,
210 ),
211 ),
212 Layer.provideMerge(LiveActivities.layer),207 Layer.provideMerge(LiveActivities.layer),
213 Layer.provideMerge(DeliveryAttempts.layer),208 Layer.provideMerge(DeliveryAttempts.layer),
214 Layer.provideMerge(RelayTokens.layer),209 Layer.provideMerge(RelayTokens.layer),
215 Layer.provideMerge(Layer.succeed(RelayDb, db)),210 Layer.provideMerge(Layer.succeed(RelayDb.RelayDb, db)),
216 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),211 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),
217 Layer.provideMerge(webcryptoLayer),212 Layer.provideMerge(webcryptoLayer),
218 );213 );
packages/client-runtime/src/state/preview.ts+4−0
@@ -37,6 +37,10 @@ export function createPreviewEnvironmentAtoms<R, E>( · 36 unmodified lines
37 automationRequests: createEnvironmentRpcSubscriptionAtomFamily(runtime, {37 automationRequests: createEnvironmentRpcSubscriptionAtomFamily(runtime, {
38 label: "environment-data:preview:automation-requests",38 label: "environment-data:preview:automation-requests",
39 tag: WS_METHODS.previewAutomationConnect,39 tag: WS_METHODS.previewAutomationConnect,
40 // Automation requests are commands, not cached query data. Dispose the
41 // stream immediately with its owner so stale requests cannot replay when
42 // a thread remounts and the server can clear disconnected hosts promptly.
43 idleTtlMs: 0,
40 }),44 }),
41 open: createEnvironmentRpcCommand(runtime, {45 open: createEnvironmentRpcCommand(runtime, {
42 label: "environment-data:preview:open",46 label: "environment-data:preview:open",
packages/contracts/src/git.ts+2−0
@@ -125,6 +125,8 @@ export const VcsListRefsInput = Schema.Struct({ · 124 unmodified lines
125 cwd: TrimmedNonEmptyStringSchema,125 cwd: TrimmedNonEmptyStringSchema,
126 query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),126 query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),
127 cursor: Schema.optional(NonNegativeInt),127 cursor: Schema.optional(NonNegativeInt),
128 includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),
129 refKind: Schema.optional(Schema.Literals(["all", "local", "remote"])),
128 limit: Schema.optional(130 limit: Schema.optional(
129 PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),131 PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),
130 ),132 ),
packages/contracts/src/ipc.ts+2−1
@@ -75,6 +75,7 @@ import { · 74 unmodified lines
75 PreviewAutomationClickInput,75 PreviewAutomationClickInput,
76 PreviewAutomationEvaluateInput,76 PreviewAutomationEvaluateInput,
77 PreviewAutomationOwner,77 PreviewAutomationOwner,
78 PreviewAutomationOwnerIdentity,
78 PreviewAutomationPressInput,79 PreviewAutomationPressInput,
79 PreviewAutomationRequest,80 PreviewAutomationRequest,
80 PreviewAutomationResponse,81 PreviewAutomationResponse,
@@ -1232,7 +1233,7 @@ export interface EnvironmentApi { · 1151 unmodified lines
1232 ) => () => void;1233 ) => () => void;
1233 respond: (response: PreviewAutomationResponse) => Promise<void>;1234 respond: (response: PreviewAutomationResponse) => Promise<void>;
1234 reportOwner: (owner: PreviewAutomationOwner) => Promise<void>;1235 reportOwner: (owner: PreviewAutomationOwner) => Promise<void>;
1235 clearOwner: (input: { clientId: string }) => Promise<void>;1236 clearOwner: (input: PreviewAutomationOwnerIdentity) => Promise<void>;
1236 };1237 };
1237 onEvent: (1238 onEvent: (
1238 callback: (event: PreviewEvent) => void,1239 callback: (event: PreviewEvent) => void,
packages/contracts/src/previewAutomation.ts+6−1
@@ -410,10 +410,15 @@ export const PreviewAutomationRecordingArtifact = Schema.Struct({ · 409 unmodified lines
410});410});
411export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type;411export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type;
412 412
413export const PreviewAutomationOwner = Schema.Struct({413export const PreviewAutomationOwnerIdentity = Schema.Struct({
414 clientId: TrimmedNonEmptyString,414 clientId: TrimmedNonEmptyString,
415 environmentId: EnvironmentId,415 environmentId: EnvironmentId,
416 threadId: ThreadId,416 threadId: ThreadId,
417});
418export type PreviewAutomationOwnerIdentity = typeof PreviewAutomationOwnerIdentity.Type;
419
420export const PreviewAutomationOwner = Schema.Struct({
421 ...PreviewAutomationOwnerIdentity.fields,
417 tabId: Schema.NullOr(PreviewTabId),422 tabId: Schema.NullOr(PreviewTabId),
418 visible: Schema.Boolean,423 visible: Schema.Boolean,
419 supportsAutomation: Schema.Boolean,424 supportsAutomation: Schema.Boolean,
packages/contracts/src/relay.ts+11−15
@@ -1,5 +1,5 @@
1import * as Schema from "effect/Schema";
2import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Schema from "effect/Schema";
3import * as HttpApi from "effect/unstable/httpapi/HttpApi";3import * as HttpApi from "effect/unstable/httpapi/HttpApi";
4import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";4import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
5import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";5import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
@@ -512,26 +512,22 @@ const RelayAgentActivityPublishErrors = [ · 506 unmodified lines
512 RelayInternalError,512 RelayInternalError,
513] as const;513] as const;
514 514
515export interface RelayClientPrincipalShape {
516 readonly userId: string;
517 readonly token: string;
518 readonly proofKeyThumbprint?: string;
519 readonly dpopScopes?: ReadonlyArray<RelayDpopAccessTokenScope>;
520}
521
522export class RelayClientPrincipal extends Context.Service<515export class RelayClientPrincipal extends Context.Service<
523 RelayClientPrincipal,516 RelayClientPrincipal,
524 RelayClientPrincipalShape517 {
518 readonly userId: string;
519 readonly token: string;
520 readonly proofKeyThumbprint?: string;
521 readonly dpopScopes?: ReadonlyArray<RelayDpopAccessTokenScope>;
522 }
525>()("@t3tools/contracts/relay/RelayClientPrincipal") {}523>()("@t3tools/contracts/relay/RelayClientPrincipal") {}
526 524
527export interface RelayEnvironmentPrincipalShape {
528 readonly environmentId: string;
529 readonly environmentPublicKey: string;
530}
531
532export class RelayEnvironmentPrincipal extends Context.Service<525export class RelayEnvironmentPrincipal extends Context.Service<
533 RelayEnvironmentPrincipal,526 RelayEnvironmentPrincipal,
534 RelayEnvironmentPrincipalShape527 {
528 readonly environmentId: string;
529 readonly environmentPublicKey: string;
530 }
535>()("@t3tools/contracts/relay/RelayEnvironmentPrincipal") {}531>()("@t3tools/contracts/relay/RelayEnvironmentPrincipal") {}
536 532
537const RelayClientBearerAuthorization = HttpApiSecurity.http({ scheme: "bearer" }).pipe(533const RelayClientBearerAuthorization = HttpApiSecurity.http({ scheme: "bearer" }).pipe(
packages/contracts/src/review.ts+1−0
@@ -6,6 +6,7 @@ import { VcsError } from "./vcs.ts"; · 5 unmodified lines
6export const ReviewDiffPreviewInput = Schema.Struct({6export const ReviewDiffPreviewInput = Schema.Struct({
7 cwd: TrimmedNonEmptyString,7 cwd: TrimmedNonEmptyString,
8 baseRef: Schema.optional(TrimmedNonEmptyString),8 baseRef: Schema.optional(TrimmedNonEmptyString),
9 ignoreWhitespace: Schema.optionalKey(Schema.Boolean),
9});10});
10export type ReviewDiffPreviewInput = typeof ReviewDiffPreviewInput.Type;11export type ReviewDiffPreviewInput = typeof ReviewDiffPreviewInput.Type;
11 12
packages/contracts/src/rpc.ts+3−2
@@ -108,6 +108,7 @@ import { · 107 unmodified lines
108import {108import {
109 PreviewAutomationError,109 PreviewAutomationError,
110 PreviewAutomationOwner,110 PreviewAutomationOwner,
111 PreviewAutomationOwnerIdentity,
111 PreviewAutomationRequest,112 PreviewAutomationRequest,
112 PreviewAutomationResponse,113 PreviewAutomationResponse,
113} from "./previewAutomation.ts";114} from "./previewAutomation.ts";
@@ -549,7 +550,7 @@ export const WsPreviewReportStatusRpc = Rpc.make(WS_METHODS.previewReportStatus, · 435 unmodified lines
549});550});
550 551
551export const WsPreviewAutomationConnectRpc = Rpc.make(WS_METHODS.previewAutomationConnect, {552export const WsPreviewAutomationConnectRpc = Rpc.make(WS_METHODS.previewAutomationConnect, {
552 payload: Schema.Struct({ clientId: Schema.String }),553 payload: PreviewAutomationOwner,
553 success: PreviewAutomationRequest,554 success: PreviewAutomationRequest,
554 error: Schema.Union([PreviewAutomationError, EnvironmentAuthorizationError]),555 error: Schema.Union([PreviewAutomationError, EnvironmentAuthorizationError]),
555 stream: true,556 stream: true,
@@ -566,7 +567,7 @@ export const WsPreviewAutomationReportOwnerRpc = Rpc.make(WS_METHODS.previewAuto · 10 unmodified lines
566});567});
567 568
568export const WsPreviewAutomationClearOwnerRpc = Rpc.make(WS_METHODS.previewAutomationClearOwner, {569export const WsPreviewAutomationClearOwnerRpc = Rpc.make(WS_METHODS.previewAutomationClearOwner, {
569 payload: Schema.Struct({ clientId: Schema.String }),570 payload: PreviewAutomationOwnerIdentity,
570 error: Schema.Union([PreviewAutomationError, EnvironmentAuthorizationError]),571 error: Schema.Union([PreviewAutomationError, EnvironmentAuthorizationError]),
571});572});
572 573
pnpm-lock.yaml+250−252
@@ -194,10 +194,10 @@ importers: · 193 unmodified lines
194 dependencies:194 dependencies:
195 '@callstack/liquid-glass':195 '@callstack/liquid-glass':
196 specifier: ^0.7.1196 specifier: ^0.7.1
197 version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)197 version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
198 '@clerk/expo':198 '@clerk/expo':
199 specifier: 3.4.8-snapshot.v20260619001138199 specifier: 3.4.8-snapshot.v20260619001138
200 version: 3.4.8-snapshot.v20260619001138(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)200 version: 3.4.8-snapshot.v20260619001138(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
201 '@effect/atom-react':201 '@effect/atom-react':
202 specifier: 4.0.0-beta.78202 specifier: 4.0.0-beta.78
203 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0)203 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0)
@@ -206,10 +206,10 @@ importers: · 2 unmodified lines
206 version: 0.4.2206 version: 0.4.2
207 '@expo/ui':207 '@expo/ui':
208 specifier: ~56.0.8208 specifier: ~56.0.8
209 version: 56.0.15(961c4aa6f32829b318e3c87ef20ad401)209 version: 56.0.15(ab3f255d102c60ba0fd2bbe4e47ba584)
210 '@legendapp/list':210 '@legendapp/list':
211 specifier: 3.0.0-beta.44211 specifier: 3.0.0-beta.44
212 version: 3.0.0-beta.44(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)212 version: 3.0.0-beta.44(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
213 '@noble/curves':213 '@noble/curves':
214 specifier: 'catalog:'214 specifier: 'catalog:'
215 version: 1.9.1215 version: 1.9.1
@@ -221,7 +221,7 @@ importers: · 5 unmodified lines
221 version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)221 version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
222 '@react-native-menu/menu':222 '@react-native-menu/menu':
223 specifier: ^2.0.0223 specifier: ^2.0.0
224 version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)224 version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
225 '@shikijs/core':225 '@shikijs/core':
226 specifier: 4.2.0226 specifier: 4.2.0
227 version: 4.2.0227 version: 4.2.0
@@ -242,7 +242,7 @@ importers: · 14 unmodified lines
242 version: link:../../packages/contracts242 version: link:../../packages/contracts
243 '@t3tools/mobile-markdown-text':243 '@t3tools/mobile-markdown-text':
244 specifier: file:./modules/t3-markdown-text244 specifier: file:./modules/t3-markdown-text
245 version: file:apps/mobile/modules/t3-markdown-text(f0ff241d48c23db461fff5d47e450578)245 version: file:apps/mobile/modules/t3-markdown-text(e909ee9a8f7fbe234da80c739fca4984)
246 '@t3tools/mobile-review-diff-native':246 '@t3tools/mobile-review-diff-native':
247 specifier: file:./modules/t3-review-diff247 specifier: file:./modules/t3-review-diff
248 version: file:apps/mobile/modules/t3-review-diff248 version: file:apps/mobile/modules/t3-review-diff
@@ -263,40 +263,40 @@ importers: · 14 unmodified lines
263 version: 4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5)263 version: 4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5)
264 expo:264 expo:
265 specifier: ^56.0.0265 specifier: ^56.0.0
266 version: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)266 version: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
267 expo-asset:267 expo-asset:
268 specifier: ~56.0.15268 specifier: ~56.0.15
269 version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)269 version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)
270 expo-auth-session:270 expo-auth-session:
271 specifier: ~56.0.12271 specifier: ~56.0.12
272 version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)272 version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
273 expo-build-properties:273 expo-build-properties:
274 specifier: ~56.0.15274 specifier: ~56.0.15
275 version: 56.0.16(expo@56.0.8)275 version: 56.0.16(expo@56.0.8)
276 expo-camera:276 expo-camera:
277 specifier: ~56.0.7277 specifier: ~56.0.7
278 version: 56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)278 version: 56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
279 expo-clipboard:279 expo-clipboard:
280 specifier: ~56.0.3280 specifier: ~56.0.3
281 version: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)281 version: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
282 expo-constants:282 expo-constants:
283 specifier: ~56.0.16283 specifier: ~56.0.16
284 version: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))284 version: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
285 expo-crypto:285 expo-crypto:
286 specifier: ~56.0.4286 specifier: ~56.0.4
287 version: 56.0.4(expo@56.0.8)287 version: 56.0.4(expo@56.0.8)
288 expo-dev-client:288 expo-dev-client:
289 specifier: ~56.0.16289 specifier: ~56.0.16
290 version: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))290 version: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
291 expo-file-system:291 expo-file-system:
292 specifier: ~56.0.7292 specifier: ~56.0.7
293 version: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))293 version: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
294 expo-font:294 expo-font:
295 specifier: ~56.0.5295 specifier: ~56.0.5
296 version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)296 version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
297 expo-glass-effect:297 expo-glass-effect:
298 specifier: ~56.0.4298 specifier: ~56.0.4
299 version: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)299 version: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
300 expo-haptics:300 expo-haptics:
301 specifier: ~56.0.3301 specifier: ~56.0.3
302 version: 56.0.3(expo@56.0.8)302 version: 56.0.3(expo@56.0.8)
@@ -305,19 +305,19 @@ importers: · 2 unmodified lines
305 version: 56.0.15(expo@56.0.8)305 version: 56.0.15(expo@56.0.8)
306 expo-linking:306 expo-linking:
307 specifier: ~56.0.12307 specifier: ~56.0.12
308 version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)308 version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
309 expo-network:309 expo-network:
310 specifier: ~56.0.5310 specifier: ~56.0.5
311 version: 56.0.5(expo@56.0.8)(react@19.2.3)311 version: 56.0.5(expo@56.0.8)(react@19.2.3)
312 expo-notifications:312 expo-notifications:
313 specifier: ~56.0.14313 specifier: ~56.0.14
314 version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)314 version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)
315 expo-paste-input:315 expo-paste-input:
316 specifier: ^0.1.15316 specifier: ^0.1.15
317 version: 0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)317 version: 0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
318 expo-router:318 expo-router:
319 specifier: ~56.2.7319 specifier: ~56.2.7
320 version: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310)320 version: 56.2.8(c021de11d02907bd585610408f5252e8)
321 expo-secure-store:321 expo-secure-store:
322 specifier: ~56.0.4322 specifier: ~56.0.4
323 version: 56.0.4(expo@56.0.8)323 version: 56.0.4(expo@56.0.8)
@@ -326,16 +326,16 @@ importers: · 2 unmodified lines
326 version: 56.0.10(expo@56.0.8)(typescript@6.0.3)326 version: 56.0.10(expo@56.0.8)(typescript@6.0.3)
327 expo-symbols:327 expo-symbols:
328 specifier: ~56.0.5328 specifier: ~56.0.5
329 version: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)329 version: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
330 expo-updates:330 expo-updates:
331 specifier: ~56.0.17331 specifier: ~56.0.17
332 version: 56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)332 version: 56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
333 expo-web-browser:333 expo-web-browser:
334 specifier: ~56.0.5334 specifier: ~56.0.5
335 version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))335 version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
336 expo-widgets:336 expo-widgets:
337 specifier: ~56.0.15337 specifier: ~56.0.15
338 version: 56.0.16(961c4aa6f32829b318e3c87ef20ad401)338 version: 56.0.16(ab3f255d102c60ba0fd2bbe4e47ba584)
339 punycode:339 punycode:
340 specifier: ^2.3.1340 specifier: ^2.3.1
341 version: 2.3.1341 version: 2.3.1
@@ -347,43 +347,43 @@ importers: · 5 unmodified lines
347 version: 19.2.3(react@19.2.3)347 version: 19.2.3(react@19.2.3)
348 react-native:348 react-native:
349 specifier: 0.85.3349 specifier: 0.85.3
350 version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)350 version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
351 react-native-gesture-handler:351 react-native-gesture-handler:
352 specifier: ~2.31.1352 specifier: ~2.31.1
353 version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)353 version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
354 react-native-image-viewing:354 react-native-image-viewing:
355 specifier: ^0.2.2355 specifier: ^0.2.2
356 version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)356 version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
357 react-native-keyboard-controller:357 react-native-keyboard-controller:
358 specifier: 1.21.6358 specifier: 1.21.6
359 version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)359 version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
360 react-native-nitro-markdown:360 react-native-nitro-markdown:
361 specifier: ^0.5.0361 specifier: ^0.5.0
362 version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)362 version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
363 react-native-nitro-modules:363 react-native-nitro-modules:
364 specifier: ^0.35.4364 specifier: ^0.35.4
365 version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)365 version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
366 react-native-reanimated:366 react-native-reanimated:
367 specifier: 4.3.1367 specifier: 4.3.1
368 version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)368 version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
369 react-native-safe-area-context:369 react-native-safe-area-context:
370 specifier: ~5.7.0370 specifier: ~5.7.0
371 version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)371 version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
372 react-native-screens:372 react-native-screens:
373 specifier: 4.25.2373 specifier: 4.25.2
374 version: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)374 version: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
375 react-native-shiki-engine:375 react-native-shiki-engine:
376 specifier: ^0.3.12376 specifier: ^0.3.12
377 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)377 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
378 react-native-svg:378 react-native-svg:
379 specifier: 15.15.4379 specifier: 15.15.4
380 version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)380 version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
381 react-native-webview:381 react-native-webview:
382 specifier: ^13.16.1382 specifier: ^13.16.1
383 version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)383 version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
384 react-native-worklets:384 react-native-worklets:
385 specifier: 0.8.3385 specifier: 0.8.3
386 version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)386 version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
387 shiki:387 shiki:
388 specifier: 4.2.0388 specifier: 4.2.0
389 version: 4.2.0389 version: 4.2.0
@@ -392,7 +392,7 @@ importers: · 2 unmodified lines
392 version: 3.6.0392 version: 3.6.0
393 uniwind:393 uniwind:
394 specifier: ^1.6.2394 specifier: ^1.6.2
395 version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0)395 version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0)
396 devDependencies:396 devDependencies:
397 '@effect/vitest':397 '@effect/vitest':
398 specifier: 4.0.0-beta.78398 specifier: 4.0.0-beta.78
@@ -10839,10 +10839,10 @@ snapshots: · 10440 unmodified lines
10839 optionalDependencies:10839 optionalDependencies:
10840 '@types/react': 19.2.1610840 '@types/react': 19.2.16
10841 10841
10842 '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':10842 '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
10843 dependencies:10843 dependencies:
10844 react: 19.2.310844 react: 19.2.3
10845 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)10845 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
10846 10846
10847 '@capsizecss/unpack@4.0.0':10847 '@capsizecss/unpack@4.0.0':
10848 dependencies:10848 dependencies:
@@ -10946,23 +10946,23 @@ snapshots: · 97 unmodified lines
10946 electron-store: 8.2.010946 electron-store: 8.2.0
10947 react-dom: 19.2.6(react@19.2.6)10947 react-dom: 19.2.6(react@19.2.6)
10948 10948
10949 '@clerk/expo@3.4.8-snapshot.v20260619001138(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':10949 '@clerk/expo@3.4.8-snapshot.v20260619001138(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
10950 dependencies:10950 dependencies:
10951 '@clerk/clerk-js': 6.18.2-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)10951 '@clerk/clerk-js': 6.18.2-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
10952 '@clerk/react': 6.10.4-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)10952 '@clerk/react': 6.10.4-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
10953 '@clerk/shared': 4.19.2-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)10953 '@clerk/shared': 4.19.2-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
10954 base-64: 1.0.010954 base-64: 1.0.0
10955 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)10955 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
10956 react: 19.2.310956 react: 19.2.3
10957 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)10957 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
10958 react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))10958 react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
10959 tslib: 2.8.110959 tslib: 2.8.1
10960 optionalDependencies:10960 optionalDependencies:
10961 expo-auth-session: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)10961 expo-auth-session: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
10962 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))10962 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
10963 expo-crypto: 56.0.4(expo@56.0.8)10963 expo-crypto: 56.0.4(expo@56.0.8)
10964 expo-secure-store: 56.0.4(expo@56.0.8)10964 expo-secure-store: 56.0.4(expo@56.0.8)
10965 expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))10965 expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
10966 react-dom: 19.2.3(react@19.2.3)10966 react-dom: 19.2.3(react@19.2.3)
10967 10967
10968 '@clerk/react@6.10.4-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':10968 '@clerk/react@6.10.4-snapshot.v20260619001138(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
@@ -11529,7 +11529,7 @@ snapshots: · 560 unmodified lines
11529 11529
11530 '@expo-google-fonts/material-symbols@0.4.38': {}11530 '@expo-google-fonts/material-symbols@0.4.38': {}
11531 11531
11532 '@expo/cli@56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)':11532 '@expo/cli@56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)':
11533 dependencies:11533 dependencies:
11534 '@expo/code-signing-certificates': 0.0.611534 '@expo/code-signing-certificates': 0.0.6
11535 '@expo/config': 56.0.9(typescript@6.0.3)11535 '@expo/config': 56.0.9(typescript@6.0.3)
@@ -11539,7 +11539,7 @@ snapshots: · 3 unmodified lines
11539 '@expo/image-utils': 0.10.1(typescript@6.0.3)11539 '@expo/image-utils': 0.10.1(typescript@6.0.3)
11540 '@expo/inline-modules': 0.0.10(typescript@6.0.3)11540 '@expo/inline-modules': 0.0.10(typescript@6.0.3)
11541 '@expo/json-file': 10.2.011541 '@expo/json-file': 10.2.0
11542 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11542 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11543 '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)11543 '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
11544 '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)11544 '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)
11545 '@expo/metro-file-map': 56.0.311545 '@expo/metro-file-map': 56.0.3
@@ -11564,7 +11564,7 @@ snapshots: · 18 unmodified lines
11564 connect: 3.7.011564 connect: 3.7.0
11565 debug: 4.4.311565 debug: 4.4.3
11566 dnssd-advertise: 1.1.411566 dnssd-advertise: 1.1.4
11567 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11567 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11568 expo-server: 56.0.411568 expo-server: 56.0.4
11569 fetch-nodeshim: 0.4.1011569 fetch-nodeshim: 0.4.10
11570 getenv: 2.0.011570 getenv: 2.0.0
@@ -11590,8 +11590,8 @@ snapshots: · 19 unmodified lines
11590 ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)11590 ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
11591 zod: 3.25.7611591 zod: 3.25.76
11592 optionalDependencies:11592 optionalDependencies:
11593 expo-router: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310)11593 expo-router: 56.2.8(c021de11d02907bd585610408f5252e8)
11594 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11594 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11595 transitivePeerDependencies:11595 transitivePeerDependencies:
11596 - '@expo/dom-webview'11596 - '@expo/dom-webview'
11597 - '@expo/metro-runtime'11597 - '@expo/metro-runtime'
@@ -11653,18 +11653,18 @@ snapshots: · 55 unmodified lines
11653 transitivePeerDependencies:11653 transitivePeerDependencies:
11654 - supports-color11654 - supports-color
11655 11655
11656 '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':11656 '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
11657 dependencies:11657 dependencies:
11658 chalk: 4.1.211658 chalk: 4.1.2
11659 optionalDependencies:11659 optionalDependencies:
11660 react: 19.2.311660 react: 19.2.3
11661 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11661 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11662 11662
11663 '@expo/dom-webview@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':11663 '@expo/dom-webview@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
11664 dependencies:11664 dependencies:
11665 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11665 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11666 react: 19.2.311666 react: 19.2.3
11667 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11667 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11668 11668
11669 '@expo/env@2.3.0':11669 '@expo/env@2.3.0':
11670 dependencies:11670 dependencies:
@@ -11725,13 +11725,13 @@ snapshots: · 54 unmodified lines
11725 - supports-color11725 - supports-color
11726 - typescript11726 - typescript
11727 11727
11728 '@expo/log-box@56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':11728 '@expo/log-box@56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
11729 dependencies:11729 dependencies:
11730 '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11730 '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11731 anser: 1.4.1011731 anser: 1.4.10
11732 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11732 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11733 react: 19.2.311733 react: 19.2.3
11734 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11734 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11735 stacktrace-parser: 0.1.1111735 stacktrace-parser: 0.1.11
11736 11736
11737 '@expo/metro-config@56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)':11737 '@expo/metro-config@56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)':
@@ -11761,7 +11761,7 @@ snapshots: · 23 unmodified lines
11761 postcss: 8.5.1511761 postcss: 8.5.15
11762 resolve-from: 5.0.011762 resolve-from: 5.0.0
11763 optionalDependencies:11763 optionalDependencies:
11764 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11764 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11765 transitivePeerDependencies:11765 transitivePeerDependencies:
11766 - bufferutil11766 - bufferutil
11767 - supports-color11767 - supports-color
@@ -11779,14 +11779,14 @@ snapshots: · 11 unmodified lines
11779 transitivePeerDependencies:11779 transitivePeerDependencies:
11780 - supports-color11780 - supports-color
11781 11781
11782 '@expo/metro-runtime@56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':11782 '@expo/metro-runtime@56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
11783 dependencies:11783 dependencies:
11784 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11784 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11785 anser: 1.4.1011785 anser: 1.4.10
11786 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11786 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11787 pretty-format: 29.7.011787 pretty-format: 29.7.0
11788 react: 19.2.311788 react: 19.2.3
11789 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11789 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11790 stacktrace-parser: 0.1.1111790 stacktrace-parser: 0.1.11
11791 whatwg-fetch: 3.6.2011791 whatwg-fetch: 3.6.20
11792 optionalDependencies:11792 optionalDependencies:
@@ -11861,14 +11861,14 @@ snapshots: · 68 unmodified lines
11861 '@expo/router-server@56.0.12(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo-server@56.0.4)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':11861 '@expo/router-server@56.0.12(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo-server@56.0.4)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
11862 dependencies:11862 dependencies:
11863 debug: 4.4.311863 debug: 4.4.3
11864 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11864 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11865 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))11865 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
11866 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11866 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11867 expo-server: 56.0.411867 expo-server: 56.0.4
11868 react: 19.2.311868 react: 19.2.3
11869 optionalDependencies:11869 optionalDependencies:
11870 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11870 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11871 expo-router: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310)11871 expo-router: 56.2.8(c021de11d02907bd585610408f5252e8)
11872 react-dom: 19.2.3(react@19.2.3)11872 react-dom: 19.2.3(react@19.2.3)
11873 transitivePeerDependencies:11873 transitivePeerDependencies:
11874 - supports-color11874 - supports-color
@@ -11883,18 +11883,18 @@ snapshots: · 8 unmodified lines
11883 11883
11884 '@expo/sudo-prompt@9.3.2': {}11884 '@expo/sudo-prompt@9.3.2': {}
11885 11885
11886 '@expo/ui@56.0.15(961c4aa6f32829b318e3c87ef20ad401)':11886 '@expo/ui@56.0.15(ab3f255d102c60ba0fd2bbe4e47ba584)':
11887 dependencies:11887 dependencies:
11888 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)11888 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
11889 react: 19.2.311889 react: 19.2.3
11890 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)11890 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
11891 sf-symbols-typescript: 2.2.011891 sf-symbols-typescript: 2.2.0
11892 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)11892 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
11893 optionalDependencies:11893 optionalDependencies:
11894 '@babel/core': 7.29.711894 '@babel/core': 7.29.7
11895 react-dom: 19.2.3(react@19.2.3)11895 react-dom: 19.2.3(react@19.2.3)
11896 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11896 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11897 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)11897 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
11898 transitivePeerDependencies:11898 transitivePeerDependencies:
11899 - '@types/react'11899 - '@types/react'
11900 - '@types/react-dom'11900 - '@types/react-dom'
@@ -12155,13 +12155,13 @@ snapshots: · 254 unmodified lines
12155 dependencies:12155 dependencies:
12156 jsbi: 4.3.212156 jsbi: 4.3.2
12157 12157
12158 '@legendapp/list@3.0.0-beta.44(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':12158 '@legendapp/list@3.0.0-beta.44(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
12159 dependencies:12159 dependencies:
12160 react: 19.2.312160 react: 19.2.3
12161 use-sync-external-store: 1.6.0(react@19.2.3)12161 use-sync-external-store: 1.6.0(react@19.2.3)
12162 optionalDependencies:12162 optionalDependencies:
12163 react-dom: 19.2.3(react@19.2.3)12163 react-dom: 19.2.3(react@19.2.3)
12164 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)12164 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
12165 12165
12166 '@legendapp/list@3.0.0-beta.44(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':12166 '@legendapp/list@3.0.0-beta.44(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
12167 dependencies:12167 dependencies:
@@ -12984,15 +12984,15 @@ snapshots: · 816 unmodified lines
12984 prompts: 2.4.212984 prompts: 2.4.2
12985 tinyexec: 1.2.412985 tinyexec: 1.2.4
12986 12986
12987 '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':12987 '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
12988 dependencies:12988 dependencies:
12989 react: 19.2.312989 react: 19.2.3
12990 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)12990 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
12991 12991
12992 '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':12992 '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
12993 dependencies:12993 dependencies:
12994 react: 19.2.312994 react: 19.2.3
12995 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)12995 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
12996 12996
12997 '@react-native/assets-registry@0.85.3': {}12997 '@react-native/assets-registry@0.85.3': {}
12998 12998
@@ -13052,7 +13052,7 @@ snapshots: · 53 unmodified lines
13052 tinyglobby: 0.2.1713052 tinyglobby: 0.2.17
13053 yargs: 17.7.213053 yargs: 17.7.2
13054 13054
13055 '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6)':13055 '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6)':
13056 dependencies:13056 dependencies:
13057 '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)13057 '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)
13058 debug: 4.4.313058 debug: 4.4.3
@@ -13062,7 +13062,7 @@ snapshots: · 3 unmodified lines
13062 metro-core: 0.84.413062 metro-core: 0.84.4
13063 semver: 7.8.113063 semver: 7.8.1
13064 optionalDependencies:13064 optionalDependencies:
13065 '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6)13065 '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)
13066 transitivePeerDependencies:13066 transitivePeerDependencies:
13067 - bufferutil13067 - bufferutil
13068 - supports-color13068 - supports-color
@@ -13110,7 +13110,7 @@ snapshots: · 41 unmodified lines
13110 transitivePeerDependencies:13110 transitivePeerDependencies:
13111 - supports-color13111 - supports-color
13112 13112
13113 '@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6)':13113 '@react-native/metro-config@0.85.3(@babel/core@7.29.7)':
13114 dependencies:13114 dependencies:
13115 '@react-native/js-polyfills': 0.85.313115 '@react-native/js-polyfills': 0.85.3
13116 '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.29.7)13116 '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.29.7)
@@ -13118,18 +13118,16 @@ snapshots: · 1 unmodified line
13118 metro-runtime: 0.84.413118 metro-runtime: 0.84.4
13119 transitivePeerDependencies:13119 transitivePeerDependencies:
13120 - '@babel/core'13120 - '@babel/core'
13121 - bufferutil
13122 - supports-color13121 - supports-color
13123 - utf-8-validate
13124 13122
13125 '@react-native/normalize-colors@0.85.3': {}13123 '@react-native/normalize-colors@0.85.3': {}
13126 13124
13127 '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':13125 '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
13128 dependencies:13126 dependencies:
13129 invariant: 2.2.413127 invariant: 2.2.4
13130 nullthrows: 1.1.113128 nullthrows: 1.1.1
13131 react: 19.2.313129 react: 19.2.3
13132 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)13130 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
13133 optionalDependencies:13131 optionalDependencies:
13134 '@types/react': 19.2.1613132 '@types/react': 19.2.16
13135 13133
@@ -13542,15 +13540,15 @@ snapshots: · 406 unmodified lines
13542 dependencies:13540 dependencies:
13543 defer-to-connect: 2.0.113541 defer-to-connect: 2.0.1
13544 13542
13545 '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(f0ff241d48c23db461fff5d47e450578)':13543 '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(e909ee9a8f7fbe234da80c739fca4984)':
13546 dependencies:13544 dependencies:
13547 expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)13545 expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)
13548 expo-clipboard: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13546 expo-clipboard: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
13549 expo-haptics: 56.0.3(expo@56.0.8)13547 expo-haptics: 56.0.3(expo@56.0.8)
13550 expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13548 expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
13551 react: 19.2.313549 react: 19.2.3
13552 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)13550 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
13553 react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13551 react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
13554 13552
13555 '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {}13553 '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {}
13556 13554
@@ -14626,8 +14624,8 @@ snapshots: · 1069 unmodified lines
14626 react-refresh: 0.14.214624 react-refresh: 0.14.2
14627 optionalDependencies:14625 optionalDependencies:
14628 '@babel/runtime': 7.29.714626 '@babel/runtime': 7.29.7
14629 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)14627 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
14630 expo-widgets: 56.0.16(961c4aa6f32829b318e3c87ef20ad401)14628 expo-widgets: 56.0.16(ab3f255d102c60ba0fd2bbe4e47ba584)
14631 transitivePeerDependencies:14629 transitivePeerDependencies:
14632 - '@babel/core'14630 - '@babel/core'
14633 - supports-color14631 - supports-color
@@ -15518,29 +15516,29 @@ snapshots: · 884 unmodified lines
15518 15516
15519 expo-application@56.0.3(expo@56.0.8):15517 expo-application@56.0.3(expo@56.0.8):
15520 dependencies:15518 dependencies:
15521 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15519 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15522 15520
15523 expo-asset@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3):15521 expo-asset@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3):
15524 dependencies:15522 dependencies:
15525 '@expo/image-utils': 0.10.1(typescript@6.0.3)15523 '@expo/image-utils': 0.10.1(typescript@6.0.3)
15526 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15524 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15527 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15525 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15528 react: 19.2.315526 react: 19.2.3
15529 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15527 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15530 transitivePeerDependencies:15528 transitivePeerDependencies:
15531 - supports-color15529 - supports-color
15532 - typescript15530 - typescript
15533 15531
15534 expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15532 expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15535 dependencies:15533 dependencies:
15536 expo-application: 56.0.3(expo@56.0.8)15534 expo-application: 56.0.3(expo@56.0.8)
15537 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15535 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15538 expo-crypto: 56.0.4(expo@56.0.8)15536 expo-crypto: 56.0.4(expo@56.0.8)
15539 expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15537 expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15540 expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15538 expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15541 invariant: 2.2.415539 invariant: 2.2.4
15542 react: 19.2.315540 react: 19.2.3
15543 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15541 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15544 transitivePeerDependencies:15542 transitivePeerDependencies:
15545 - expo15543 - expo
15546 - supports-color15544 - supports-color
@@ -15548,119 +15546,119 @@ snapshots: · 1 unmodified line
15548 expo-build-properties@56.0.16(expo@56.0.8):15546 expo-build-properties@56.0.16(expo@56.0.8):
15549 dependencies:15547 dependencies:
15550 '@expo/schema-utils': 56.0.115548 '@expo/schema-utils': 56.0.1
15551 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15549 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15552 resolve-from: 5.0.015550 resolve-from: 5.0.0
15553 semver: 7.8.115551 semver: 7.8.1
15554 15552
15555 expo-camera@56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15553 expo-camera@56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15556 dependencies:15554 dependencies:
15557 barcode-detector: 3.2.0(@types/emscripten@1.41.5)15555 barcode-detector: 3.2.0(@types/emscripten@1.41.5)
15558 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15556 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15559 react: 19.2.315557 react: 19.2.3
15560 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15558 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15561 transitivePeerDependencies:15559 transitivePeerDependencies:
15562 - '@types/emscripten'15560 - '@types/emscripten'
15563 15561
15564 expo-clipboard@56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15562 expo-clipboard@56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15565 dependencies:15563 dependencies:
15566 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15564 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15567 react: 19.2.315565 react: 19.2.3
15568 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15566 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15569 15567
15570 expo-constants@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15568 expo-constants@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15571 dependencies:15569 dependencies:
15572 '@expo/env': 2.3.015570 '@expo/env': 2.3.0
15573 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15571 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15574 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15572 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15575 transitivePeerDependencies:15573 transitivePeerDependencies:
15576 - supports-color15574 - supports-color
15577 15575
15578 expo-crypto@56.0.4(expo@56.0.8):15576 expo-crypto@56.0.4(expo@56.0.8):
15579 dependencies:15577 dependencies:
15580 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15578 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15581 15579
15582 expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15580 expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15583 dependencies:15581 dependencies:
15584 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15582 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15585 expo-dev-launcher: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15583 expo-dev-launcher: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15586 expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15584 expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15587 expo-dev-menu-interface: 56.0.1(expo@56.0.8)15585 expo-dev-menu-interface: 56.0.1(expo@56.0.8)
15588 expo-manifests: 56.0.4(expo@56.0.8)15586 expo-manifests: 56.0.4(expo@56.0.8)
15589 expo-updates-interface: 56.0.2(expo@56.0.8)15587 expo-updates-interface: 56.0.2(expo@56.0.8)
15590 transitivePeerDependencies:15588 transitivePeerDependencies:
15591 - react-native15589 - react-native
15592 15590
15593 expo-dev-launcher@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15591 expo-dev-launcher@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15594 dependencies:15592 dependencies:
15595 '@expo/schema-utils': 56.0.115593 '@expo/schema-utils': 56.0.1
15596 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15594 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15597 expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15595 expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15598 expo-manifests: 56.0.4(expo@56.0.8)15596 expo-manifests: 56.0.4(expo@56.0.8)
15599 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15597 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15600 15598
15601 expo-dev-menu-interface@56.0.1(expo@56.0.8):15599 expo-dev-menu-interface@56.0.1(expo@56.0.8):
15602 dependencies:15600 dependencies:
15603 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15601 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15604 15602
15605 expo-dev-menu@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15603 expo-dev-menu@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15606 dependencies:15604 dependencies:
15607 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15605 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15608 expo-dev-menu-interface: 56.0.1(expo@56.0.8)15606 expo-dev-menu-interface: 56.0.1(expo@56.0.8)
15609 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15607 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15610 15608
15611 expo-eas-client@56.0.1: {}15609 expo-eas-client@56.0.1: {}
15612 15610
15613 expo-file-system@56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15611 expo-file-system@56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15614 dependencies:15612 dependencies:
15615 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15613 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15616 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15614 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15617 15615
15618 expo-font@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15616 expo-font@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15619 dependencies:15617 dependencies:
15620 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15618 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15621 fontfaceobserver: 2.3.015619 fontfaceobserver: 2.3.0
15622 react: 19.2.315620 react: 19.2.3
15623 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15621 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15624 15622
15625 expo-glass-effect@56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15623 expo-glass-effect@56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15626 dependencies:15624 dependencies:
15627 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15625 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15628 react: 19.2.315626 react: 19.2.3
15629 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15627 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15630 15628
15631 expo-haptics@56.0.3(expo@56.0.8):15629 expo-haptics@56.0.3(expo@56.0.8):
15632 dependencies:15630 dependencies:
15633 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15631 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15634 15632
15635 expo-image-loader@56.0.3(expo@56.0.8):15633 expo-image-loader@56.0.3(expo@56.0.8):
15636 dependencies:15634 dependencies:
15637 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15635 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15638 15636
15639 expo-image-picker@56.0.15(expo@56.0.8):15637 expo-image-picker@56.0.15(expo@56.0.8):
15640 dependencies:15638 dependencies:
15641 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15639 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15642 expo-image-loader: 56.0.3(expo@56.0.8)15640 expo-image-loader: 56.0.3(expo@56.0.8)
15643 15641
15644 expo-json-utils@56.0.0: {}15642 expo-json-utils@56.0.0: {}
15645 15643
15646 expo-keep-awake@56.0.3(expo@56.0.8)(react@19.2.3):15644 expo-keep-awake@56.0.3(expo@56.0.8)(react@19.2.3):
15647 dependencies:15645 dependencies:
15648 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15646 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15649 react: 19.2.315647 react: 19.2.3
15650 15648
15651 expo-linking@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15649 expo-linking@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15652 dependencies:15650 dependencies:
15653 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15651 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15654 invariant: 2.2.415652 invariant: 2.2.4
15655 react: 19.2.315653 react: 19.2.3
15656 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15654 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15657 transitivePeerDependencies:15655 transitivePeerDependencies:
15658 - expo15656 - expo
15659 - supports-color15657 - supports-color
15660 15658
15661 expo-manifests@56.0.4(expo@56.0.8):15659 expo-manifests@56.0.4(expo@56.0.8):
15662 dependencies:15660 dependencies:
15663 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15661 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15664 expo-json-utils: 56.0.015662 expo-json-utils: 56.0.0
15665 15663
15666 expo-modules-autolinking@56.0.14(typescript@6.0.3):15664 expo-modules-autolinking@56.0.14(typescript@6.0.3):
@@ -15673,66 +15671,66 @@ snapshots: · 6 unmodified lines
15673 - supports-color15671 - supports-color
15674 - typescript15672 - typescript
15675 15673
15676 expo-modules-core@56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15674 expo-modules-core@56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15677 dependencies:15675 dependencies:
15678 '@expo/expo-modules-macros-plugin': 0.0.915676 '@expo/expo-modules-macros-plugin': 0.0.9
15679 expo-modules-jsi: 56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15677 expo-modules-jsi: 56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15680 invariant: 2.2.415678 invariant: 2.2.4
15681 react: 19.2.315679 react: 19.2.3
15682 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15680 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15683 optionalDependencies:15681 optionalDependencies:
15684 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15682 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15685 15683
15686 expo-modules-jsi@56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15684 expo-modules-jsi@56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15687 dependencies:15685 dependencies:
15688 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15686 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15689 15687
15690 expo-network@56.0.5(expo@56.0.8)(react@19.2.3):15688 expo-network@56.0.5(expo@56.0.8)(react@19.2.3):
15691 dependencies:15689 dependencies:
15692 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15690 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15693 react: 19.2.315691 react: 19.2.3
15694 15692
15695 expo-notifications@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3):15693 expo-notifications@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3):
15696 dependencies:15694 dependencies:
15697 '@expo/image-utils': 0.10.1(typescript@6.0.3)15695 '@expo/image-utils': 0.10.1(typescript@6.0.3)
15698 abort-controller: 3.0.015696 abort-controller: 3.0.0
15699 badgin: 1.2.315697 badgin: 1.2.3
15700 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15698 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15701 expo-application: 56.0.3(expo@56.0.8)15699 expo-application: 56.0.3(expo@56.0.8)
15702 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15700 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15703 react: 19.2.315701 react: 19.2.3
15704 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15702 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15705 transitivePeerDependencies:15703 transitivePeerDependencies:
15706 - supports-color15704 - supports-color
15707 - typescript15705 - typescript
15708 15706
15709 expo-paste-input@0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15707 expo-paste-input@0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15710 dependencies:15708 dependencies:
15711 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15709 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15712 react: 19.2.315710 react: 19.2.3
15713 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15711 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15714 15712
15715 expo-router@56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310):15713 expo-router@56.2.8(c021de11d02907bd585610408f5252e8):
15716 dependencies:15714 dependencies:
15717 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15715 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15718 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15716 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15719 '@expo/schema-utils': 56.0.115717 '@expo/schema-utils': 56.0.1
15720 '@expo/ui': 56.0.15(961c4aa6f32829b318e3c87ef20ad401)15718 '@expo/ui': 56.0.15(ab3f255d102c60ba0fd2bbe4e47ba584)
15721 '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3)15719 '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3)
15722 '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)15720 '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
15723 '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15721 '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15724 '@testing-library/jest-dom': 6.9.115722 '@testing-library/jest-dom': 6.9.1
15725 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)15723 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
15726 client-only: 0.0.115724 client-only: 0.0.1
15727 color: 4.2.315725 color: 4.2.3
15728 debug: 4.4.315726 debug: 4.4.3
15729 escape-string-regexp: 4.0.015727 escape-string-regexp: 4.0.0
15730 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15728 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15731 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15729 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15732 expo-glass-effect: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15730 expo-glass-effect: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15733 expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15731 expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15734 expo-server: 56.0.415732 expo-server: 56.0.4
15735 expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15733 expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15736 fast-deep-equal: 3.1.315734 fast-deep-equal: 3.1.3
15737 invariant: 2.2.415735 invariant: 2.2.4
15738 nanoid: 3.3.1215736 nanoid: 3.3.12
@@ -15740,18 +15738,18 @@ snapshots: · 1 unmodified line
15740 react: 19.2.315738 react: 19.2.3
15741 react-fast-compare: 3.2.215739 react-fast-compare: 3.2.2
15742 react-is: 19.2.715740 react-is: 19.2.7
15743 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15741 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15744 react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455)15742 react-native-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15745 react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15743 react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15746 react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15744 react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15747 server-only: 0.0.115745 server-only: 0.0.1
15748 sf-symbols-typescript: 2.2.015746 sf-symbols-typescript: 2.2.0
15749 shallowequal: 1.1.015747 shallowequal: 1.1.0
15750 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)15748 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
15751 optionalDependencies:15749 optionalDependencies:
15752 react-dom: 19.2.3(react@19.2.3)15750 react-dom: 19.2.3(react@19.2.3)
15753 react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15751 react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15754 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15752 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15755 transitivePeerDependencies:15753 transitivePeerDependencies:
15756 - '@babel/core'15754 - '@babel/core'
15757 - '@testing-library/dom'15755 - '@testing-library/dom'
@@ -15763,7 +15761,7 @@ snapshots: · 5 unmodified lines
15763 15761
15764 expo-secure-store@56.0.4(expo@56.0.8):15762 expo-secure-store@56.0.4(expo@56.0.8):
15765 dependencies:15763 dependencies:
15766 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15764 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15767 15765
15768 expo-server@56.0.4: {}15766 expo-server@56.0.4: {}
15769 15767
@@ -15771,7 +15769,7 @@ snapshots: · 1 unmodified line
15771 dependencies:15769 dependencies:
15772 '@expo/config-plugins': 56.0.8(typescript@6.0.3)15770 '@expo/config-plugins': 56.0.8(typescript@6.0.3)
15773 '@expo/image-utils': 0.10.1(typescript@6.0.3)15771 '@expo/image-utils': 0.10.1(typescript@6.0.3)
15774 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15772 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15775 xml2js: 0.6.015773 xml2js: 0.6.0
15776 transitivePeerDependencies:15774 transitivePeerDependencies:
15777 - supports-color15775 - supports-color
@@ -15779,20 +15777,20 @@ snapshots: · 1 unmodified line
15779 15777
15780 expo-structured-headers@56.0.0: {}15778 expo-structured-headers@56.0.0: {}
15781 15779
15782 expo-symbols@56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15780 expo-symbols@56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15783 dependencies:15781 dependencies:
15784 '@expo-google-fonts/material-symbols': 0.4.3815782 '@expo-google-fonts/material-symbols': 0.4.38
15785 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15783 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15786 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15784 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15787 react: 19.2.315785 react: 19.2.3
15788 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15786 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15789 sf-symbols-typescript: 2.2.015787 sf-symbols-typescript: 2.2.0
15790 15788
15791 expo-updates-interface@56.0.2(expo@56.0.8):15789 expo-updates-interface@56.0.2(expo@56.0.8):
15792 dependencies:15790 dependencies:
15793 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15791 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15794 15792
15795 expo-updates@56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):15793 expo-updates@56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
15796 dependencies:15794 dependencies:
15797 '@expo/code-signing-certificates': 0.0.615795 '@expo/code-signing-certificates': 0.0.6
15798 '@expo/plist': 0.7.015796 '@expo/plist': 0.7.0
@@ -15800,7 +15798,7 @@ snapshots: · 1 unmodified line
15800 arg: 4.1.315798 arg: 4.1.3
15801 chalk: 4.1.215799 chalk: 4.1.2
15802 debug: 4.4.315800 debug: 4.4.3
15803 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15801 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15804 expo-eas-client: 56.0.115802 expo-eas-client: 56.0.1
15805 expo-manifests: 56.0.4(expo@56.0.8)15803 expo-manifests: 56.0.4(expo@56.0.8)
15806 expo-structured-headers: 56.0.015804 expo-structured-headers: 56.0.0
@@ -15810,25 +15808,25 @@ snapshots: · 3 unmodified lines
15810 ignore: 5.3.215808 ignore: 5.3.2
15811 nullthrows: 1.1.115809 nullthrows: 1.1.1
15812 react: 19.2.315810 react: 19.2.3
15813 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15811 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15814 resolve-from: 5.0.015812 resolve-from: 5.0.0
15815 optionalDependencies:15813 optionalDependencies:
15816 expo-dev-client: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15814 expo-dev-client: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15817 transitivePeerDependencies:15815 transitivePeerDependencies:
15818 - supports-color15816 - supports-color
15819 15817
15820 expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):15818 expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
15821 dependencies:15819 dependencies:
15822 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15820 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15823 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15821 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15824 15822
15825 expo-widgets@56.0.16(961c4aa6f32829b318e3c87ef20ad401):15823 expo-widgets@56.0.16(ab3f255d102c60ba0fd2bbe4e47ba584):
15826 dependencies:15824 dependencies:
15827 '@expo/plist': 0.7.015825 '@expo/plist': 0.7.0
15828 '@expo/ui': 56.0.15(961c4aa6f32829b318e3c87ef20ad401)15826 '@expo/ui': 56.0.15(ab3f255d102c60ba0fd2bbe4e47ba584)
15829 expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d)15827 expo: 56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15830 react: 19.2.315828 react: 19.2.3
15831 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15829 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15832 transitivePeerDependencies:15830 transitivePeerDependencies:
15833 - '@babel/core'15831 - '@babel/core'
15834 - '@types/react'15832 - '@types/react'
@@ -15837,37 +15835,37 @@ snapshots: · 2 unmodified lines
15837 - react-native-reanimated15835 - react-native-reanimated
15838 - react-native-worklets15836 - react-native-worklets
15839 15837
15840 expo@56.0.8(63f7aade424ad9e7b1154b679fa2a14d):15838 expo@56.0.8(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-router@56.2.8)(expo-widgets@56.0.16)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6):
15841 dependencies:15839 dependencies:
15842 '@babel/runtime': 7.29.715840 '@babel/runtime': 7.29.7
15843 '@expo/cli': 56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)15841 '@expo/cli': 56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)
15844 '@expo/config': 56.0.9(typescript@6.0.3)15842 '@expo/config': 56.0.9(typescript@6.0.3)
15845 '@expo/config-plugins': 56.0.8(typescript@6.0.3)15843 '@expo/config-plugins': 56.0.8(typescript@6.0.3)
15846 '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15844 '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15847 '@expo/fingerprint': 0.19.315845 '@expo/fingerprint': 0.19.3
15848 '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3)15846 '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3)
15849 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15847 '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15850 '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)15848 '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
15851 '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)15849 '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)
15852 '@ungap/structured-clone': 1.3.115850 '@ungap/structured-clone': 1.3.1
15853 babel-preset-expo: 56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.16)(expo@56.0.8)(react-refresh@0.14.2)15851 babel-preset-expo: 56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.16)(expo@56.0.8)(react-refresh@0.14.2)
15854 expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)15852 expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)
15855 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15853 expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15856 expo-file-system: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))15854 expo-file-system: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
15857 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15855 expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15858 expo-keep-awake: 56.0.3(expo@56.0.8)(react@19.2.3)15856 expo-keep-awake: 56.0.3(expo@56.0.8)(react@19.2.3)
15859 expo-modules-autolinking: 56.0.14(typescript@6.0.3)15857 expo-modules-autolinking: 56.0.14(typescript@6.0.3)
15860 expo-modules-core: 56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15858 expo-modules-core: 56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15861 pretty-format: 29.7.015859 pretty-format: 29.7.0
15862 react: 19.2.315860 react: 19.2.3
15863 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)15861 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
15864 react-refresh: 0.14.215862 react-refresh: 0.14.2
15865 whatwg-url-minimum: 0.1.215863 whatwg-url-minimum: 0.1.2
15866 optionalDependencies:15864 optionalDependencies:
15867 '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15865 '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15868 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15866 '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15869 react-dom: 19.2.3(react@19.2.3)15867 react-dom: 19.2.3(react@19.2.3)
15870 react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)15868 react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
15871 transitivePeerDependencies:15869 transitivePeerDependencies:
15872 - '@babel/core'15870 - '@babel/core'
15873 - bufferutil15871 - bufferutil
@@ -18285,102 +18283,102 @@ snapshots: · 2411 unmodified lines
18285 transitivePeerDependencies:18283 transitivePeerDependencies:
18286 - supports-color18284 - supports-color
18287 18285
18288 react-native-drawer-layout@4.2.4(0e9729601f58a7a7ae26c76fe6017455):18286 react-native-drawer-layout@4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18289 dependencies:18287 dependencies:
18290 color: 4.2.318288 color: 4.2.3
18291 react: 19.2.318289 react: 19.2.3
18292 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18290 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18293 react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18291 react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18294 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18292 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18295 use-latest-callback: 0.2.6(react@19.2.3)18293 use-latest-callback: 0.2.6(react@19.2.3)
18296 18294
18297 react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18295 react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18298 dependencies:18296 dependencies:
18299 '@egjs/hammerjs': 2.0.1718297 '@egjs/hammerjs': 2.0.17
18300 '@types/react-test-renderer': 19.1.018298 '@types/react-test-renderer': 19.1.0
18301 hoist-non-react-statics: 3.3.218299 hoist-non-react-statics: 3.3.2
18302 invariant: 2.2.418300 invariant: 2.2.4
18303 react: 19.2.318301 react: 19.2.3
18304 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18302 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18305 18303
18306 react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18304 react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18307 dependencies:18305 dependencies:
18308 react: 19.2.318306 react: 19.2.3
18309 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18307 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18310 18308
18311 react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18309 react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18312 dependencies:18310 dependencies:
18313 react: 19.2.318311 react: 19.2.3
18314 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18312 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18315 18313
18316 react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18314 react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18317 dependencies:18315 dependencies:
18318 react: 19.2.318316 react: 19.2.3
18319 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18317 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18320 react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18318 react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18321 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18319 react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18322 18320
18323 react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18321 react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18324 dependencies:18322 dependencies:
18325 react: 19.2.318323 react: 19.2.3
18326 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18324 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18327 react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18325 react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18328 optionalDependencies:18326 optionalDependencies:
18329 react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18327 react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18330 18328
18331 react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18329 react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18332 dependencies:18330 dependencies:
18333 react: 19.2.318331 react: 19.2.3
18334 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18332 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18335 18333
18336 react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18334 react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18337 dependencies:18335 dependencies:
18338 react: 19.2.318336 react: 19.2.3
18339 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18337 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18340 react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18338 react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18341 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18339 react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18342 semver: 7.8.118340 semver: 7.8.1
18343 18341
18344 react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18342 react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18345 dependencies:18343 dependencies:
18346 react: 19.2.318344 react: 19.2.3
18347 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18345 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18348 18346
18349 react-native-screens@4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18347 react-native-screens@4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18350 dependencies:18348 dependencies:
18351 react: 19.2.318349 react: 19.2.3
18352 react-freeze: 1.0.4(react@19.2.3)18350 react-freeze: 1.0.4(react@19.2.3)
18353 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18351 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18354 warn-once: 0.1.118352 warn-once: 0.1.1
18355 18353
18356 react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18354 react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18357 dependencies:18355 dependencies:
18358 '@shikijs/types': 4.2.018356 '@shikijs/types': 4.2.0
18359 '@shikijs/vscode-textmate': 10.0.218357 '@shikijs/vscode-textmate': 10.0.2
18360 react: 19.2.318358 react: 19.2.3
18361 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18359 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18362 18360
18363 react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18361 react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18364 dependencies:18362 dependencies:
18365 css-select: 5.2.218363 css-select: 5.2.2
18366 css-tree: 1.1.318364 css-tree: 1.1.3
18367 react: 19.2.318365 react: 19.2.3
18368 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18366 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18369 warn-once: 0.1.118367 warn-once: 0.1.1
18370 18368
18371 react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):18369 react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)):
18372 dependencies:18370 dependencies:
18373 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18371 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18374 whatwg-url-without-unicode: 8.0.0-318372 whatwg-url-without-unicode: 8.0.0-3
18375 18373
18376 react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18374 react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18377 dependencies:18375 dependencies:
18378 escape-string-regexp: 4.0.018376 escape-string-regexp: 4.0.0
18379 invariant: 2.2.418377 invariant: 2.2.4
18380 react: 19.2.318378 react: 19.2.3
18381 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18379 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18382 18380
18383 react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):18381 react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
18384 dependencies:18382 dependencies:
18385 '@babel/core': 7.29.718383 '@babel/core': 7.29.7
18386 '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7)18384 '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7)
@@ -18392,23 +18390,23 @@ snapshots: · 5 unmodified lines
18392 '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7)18390 '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7)
18393 '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7)18391 '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7)
18394 '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7)18392 '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7)
18395 '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6)18393 '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)
18396 convert-source-map: 2.0.018394 convert-source-map: 2.0.0
18397 react: 19.2.318395 react: 19.2.3
18398 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)18396 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
18399 semver: 7.8.118397 semver: 7.8.1
18400 transitivePeerDependencies:18398 transitivePeerDependencies:
18401 - supports-color18399 - supports-color
18402 18400
18403 react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6):18401 react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6):
18404 dependencies:18402 dependencies:
18405 '@react-native/assets-registry': 0.85.318403 '@react-native/assets-registry': 0.85.3
18406 '@react-native/codegen': 0.85.3(@babel/core@7.29.7)18404 '@react-native/codegen': 0.85.3(@babel/core@7.29.7)
18407 '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6)18405 '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6)
18408 '@react-native/gradle-plugin': 0.85.318406 '@react-native/gradle-plugin': 0.85.3
18409 '@react-native/js-polyfills': 0.85.318407 '@react-native/js-polyfills': 0.85.3
18410 '@react-native/normalize-colors': 0.85.318408 '@react-native/normalize-colors': 0.85.3
18411 '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)18409 '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
18412 abort-controller: 3.0.018410 abort-controller: 3.0.0
18413 anser: 1.4.1018411 anser: 1.4.10
18414 ansi-regex: 5.0.118412 ansi-regex: 5.0.1
@@ -19437,14 +19435,14 @@ snapshots: · 1022 unmodified lines
19437 19435
19438 universalify@2.0.1: {}19436 universalify@2.0.1: {}
19439 19437
19440 uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0):19438 uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0):
19441 dependencies:19439 dependencies:
19442 '@tailwindcss/node': 4.2.119440 '@tailwindcss/node': 4.2.1
19443 '@tailwindcss/oxide': 4.2.119441 '@tailwindcss/oxide': 4.2.1
19444 culori: 4.0.219442 culori: 4.0.2
19445 lightningcss: 1.30.119443 lightningcss: 1.30.1
19446 react: 19.2.319444 react: 19.2.3
19447 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)19445 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
19448 tailwindcss: 4.3.019446 tailwindcss: 4.3.0
19449 19447
19450 unpipe@1.0.0: {}19448 unpipe@1.0.0: {}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t