| author | |
| committer | |
| log | db4bf9497b524f35e665af9e941aa8faeb05ea9e |
| tree | 7ea41f309200216d073db044e37e6b1826f6bcb2 |
| parent | b8262b41228e03b2e1e14b701be449034349c8ba |
| signature | Signed by PGP key B5690EEEBB952194 |
41 files changed, 17 insertions(+), 744 deletions(-)
.github/workflows/release.yml-29| ... | @@ -851,35 +851,6 @@ jobs: | ... | @@ -851,35 +851,6 @@ jobs: |
| 851 | fi | 851 | fi |
| 852 | done | 852 | done |
| 853 | 853 | ||
| 854 | # - name: Merge Windows updater manifests | ||
| 855 | # run: | | ||
| 856 | # shopt -s nullglob | ||
| 857 | # found_windows_manifest=false | ||
| 858 | # for x64_manifest in release-assets/*-win-x64.yml; do | ||
| 859 | # if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then | ||
| 860 | # continue | ||
| 861 | # fi | ||
| 862 | |||
| 863 | # arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}" | ||
| 864 | # output_manifest="${x64_manifest/-win-x64.yml/.yml}" | ||
| 865 | # if [[ ! -f "$arm64_manifest" ]]; then | ||
| 866 | # echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2 | ||
| 867 | # exit 1 | ||
| 868 | # fi | ||
| 869 | |||
| 870 | # found_windows_manifest=true | ||
| 871 | # node scripts/merge-update-manifests.ts --platform win \ | ||
| 872 | # "$arm64_manifest" \ | ||
| 873 | # "$x64_manifest" \ | ||
| 874 | # "$output_manifest" | ||
| 875 | # rm -f "$arm64_manifest" "$x64_manifest" | ||
| 876 | # done | ||
| 877 | |||
| 878 | # if [[ "$found_windows_manifest" != true ]]; then | ||
| 879 | # echo "No Windows updater manifests found to merge." >&2 | ||
| 880 | # exit 1 | ||
| 881 | # fi | ||
| 882 | |||
| 883 | - name: Publish release | 854 | - name: Publish release |
| 884 | if: needs.preflight.outputs.previous_tag != '' | 855 | if: needs.preflight.outputs.previous_tag != '' |
| 885 | uses: softprops/action-gh-release@v3 | 856 | uses: softprops/action-gh-release@v3 |
apps/desktop/src/wsl/DesktopWslEnvironment.test.ts-3| ... | @@ -198,13 +198,10 @@ describe("WSL runtime cache", () => { | ... | @@ -198,13 +198,10 @@ describe("WSL runtime cache", () => { |
| 198 | expect(script).toContain(' [ -f "$runtime_root/apps/server/dist/bin.mjs" ] &&'); | 198 | expect(script).toContain(' [ -f "$runtime_root/apps/server/dist/bin.mjs" ] &&'); |
| 199 | expect(script).toContain(' [ -f "$runtime_root/node_modules/node-pty/package.json" ] &&'); | 199 | expect(script).toContain(' [ -f "$runtime_root/node_modules/node-pty/package.json" ] &&'); |
| 200 | expect(script).toContain(' node_pty_payload_present "$runtime_root"'); | 200 | expect(script).toContain(' node_pty_payload_present "$runtime_root"'); |
| 201 | expect(script).not.toContain("node_modules/effect/package.json"); | ||
| 202 | expect(script).toContain("if runtime_is_ready; then"); | 201 | expect(script).toContain("if runtime_is_ready; then"); |
| 203 | expect(script).toContain("trap 'exit 1' HUP INT TERM"); | 202 | expect(script).toContain("trap 'exit 1' HUP INT TERM"); |
| 204 | expect(script).toContain('exec 9> "$runtime_lock"'); | 203 | expect(script).toContain('exec 9> "$runtime_lock"'); |
| 205 | expect(script).toContain("flock -x 9"); | 204 | expect(script).toContain("flock -x 9"); |
| 206 | expect(script).not.toContain("runtime_lock_pid"); | ||
| 207 | expect(script).not.toContain("sleep 0.1"); | ||
| 208 | expect(script).not.toContain('rm -rf "$runtime_lock"'); | 205 | expect(script).not.toContain('rm -rf "$runtime_lock"'); |
| 209 | expect(script).toContain('mv -T "$runtime_root" "$runtime_stale"'); | 206 | expect(script).toContain('mv -T "$runtime_root" "$runtime_stale"'); |
| 210 | expect(script).toContain('mktemp -d "$runtime_parent/.1.2.3-x64.tmp.XXXXXX"'); | 207 | expect(script).toContain('mktemp -d "$runtime_parent/.1.2.3-x64.tmp.XXXXXX"'); |
apps/marketing/public/apple-touch-icon.webp deleted| Binary files a/apps/marketing/public/apple-touch-icon.webp and /dev/null differ | |||
apps/marketing/public/favicon-16x16.webp deleted| Binary files a/apps/marketing/public/favicon-16x16.webp and /dev/null differ | |||
apps/marketing/public/favicon-32x32.webp deleted| Binary files a/apps/marketing/public/favicon-32x32.webp and /dev/null differ | |||
apps/marketing/public/icon.png deleted| Binary files a/apps/marketing/public/icon.png and /dev/null differ | |||
apps/marketing/public/screenshot.webp deleted| Binary files a/apps/marketing/public/screenshot.webp and /dev/null differ | |||
apps/marketing/tweets.md deleted-19| ... | @@ -1,19 +0,0 @@ | ||
| 1 | https://x.com/Shay_Benshabtay/status/2054668503857156326 | ||
| 2 | https://x.com/teja2495/status/2052420254991581623 | ||
| 3 | https://x.com/developedbyed/status/2030627970532921605 | ||
| 4 | https://x.com/tannerlinsley/status/2031102771529920966 | ||
| 5 | https://x.com/aronprins/status/2045102518196183109 | ||
| 6 | https://x.com/BennettBuhner/status/2054667115697754387 | ||
| 7 | https://x.com/ex0t1clol/status/2054666870008021197 | ||
| 8 | https://x.com/Josikinz/status/2030367951694745870 | ||
| 9 | https://x.com/jetpackjoe_/status/2054666792933404959 | ||
| 10 | https://x.com/mil000/status/2030120041451246071 | ||
| 11 | https://x.com/_winter_wonders/status/2052350198764970434 | ||
| 12 | https://x.com/kostyniuk00/status/2052041388179468521 | ||
| 13 | https://x.com/gnukeith/status/2054670073579630730 | ||
| 14 | https://x.com/peculiarnewbie/status/2054671685027233827 | ||
| 15 | https://x.com/leodev/status/2054679746353537042 | ||
| 16 | https://x.com/pocarles/status/2054673964274758046 | ||
| 17 | https://x.com/DavidKPiano/status/2054682983504719930 | ||
| 18 | https://x.com/iamkaffe/status/2054675539311411280 | ||
| 19 | https://x.com/uwunetes/status/2054683356022120640 | ||
apps/mobile/package.json-1| ... | @@ -52,7 +52,6 @@ | ... | @@ -52,7 +52,6 @@ |
| 52 | "@expo/ui": "~57.0.14", | 52 | "@expo/ui": "~57.0.14", |
| 53 | "@legendapp/list": "catalog:", | 53 | "@legendapp/list": "catalog:", |
| 54 | "@noble/curves": "catalog:", | 54 | "@noble/curves": "catalog:", |
| 55 | "@noble/hashes": "catalog:", | ||
| 56 | "@pierre/diffs": "catalog:", | 55 | "@pierre/diffs": "catalog:", |
| 57 | "@react-native-ai/apple": "0.12.0", | 56 | "@react-native-ai/apple": "0.12.0", |
| 58 | "@react-native-menu/menu": "^2.0.0", | 57 | "@react-native-menu/menu": "^2.0.0", |
apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.test.ts deleted-108| ... | @@ -1,108 +0,0 @@ | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | ||
| 2 | |||
| 3 | import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; | ||
| 4 | |||
| 5 | describe("resolveTerminalRouteBootstrap", () => { | ||
| 6 | it("redirects bare terminal routes to another already-running terminal for the thread", () => { | ||
| 7 | expect( | ||
| 8 | resolveTerminalRouteBootstrap({ | ||
| 9 | hasThread: true, | ||
| 10 | hasWorkspaceRoot: true, | ||
| 11 | hasOpened: false, | ||
| 12 | requestedTerminalId: null, | ||
| 13 | currentTerminalId: "default", | ||
| 14 | runningTerminalId: "term-2", | ||
| 15 | currentTerminalStatus: "closed", | ||
| 16 | hasCurrentTerminalHydration: false, | ||
| 17 | }), | ||
| 18 | ).toEqual({ | ||
| 19 | kind: "redirect", | ||
| 20 | terminalId: "term-2", | ||
| 21 | }); | ||
| 22 | }); | ||
| 23 | |||
| 24 | it("hydrates the current running terminal when client state is not hydrated yet", () => { | ||
| 25 | expect( | ||
| 26 | resolveTerminalRouteBootstrap({ | ||
| 27 | hasThread: true, | ||
| 28 | hasWorkspaceRoot: true, | ||
| 29 | hasOpened: false, | ||
| 30 | requestedTerminalId: null, | ||
| 31 | currentTerminalId: "default", | ||
| 32 | runningTerminalId: "default", | ||
| 33 | currentTerminalStatus: "running", | ||
| 34 | hasCurrentTerminalHydration: false, | ||
| 35 | }), | ||
| 36 | ).toEqual({ | ||
| 37 | kind: "open", | ||
| 38 | }); | ||
| 39 | }); | ||
| 40 | |||
| 41 | it("opens explicit terminal routes when the session still needs hydration", () => { | ||
| 42 | expect( | ||
| 43 | resolveTerminalRouteBootstrap({ | ||
| 44 | hasThread: true, | ||
| 45 | hasWorkspaceRoot: true, | ||
| 46 | hasOpened: false, | ||
| 47 | requestedTerminalId: "term-2", | ||
| 48 | currentTerminalId: "term-2", | ||
| 49 | runningTerminalId: "term-2", | ||
| 50 | currentTerminalStatus: "running", | ||
| 51 | hasCurrentTerminalHydration: false, | ||
| 52 | }), | ||
| 53 | ).toEqual({ | ||
| 54 | kind: "open", | ||
| 55 | }); | ||
| 56 | }); | ||
| 57 | |||
| 58 | it("stays idle after the route already bootstrapped once", () => { | ||
| 59 | expect( | ||
| 60 | resolveTerminalRouteBootstrap({ | ||
| 61 | hasThread: true, | ||
| 62 | hasWorkspaceRoot: true, | ||
| 63 | hasOpened: true, | ||
| 64 | requestedTerminalId: null, | ||
| 65 | currentTerminalId: "default", | ||
| 66 | runningTerminalId: "default", | ||
| 67 | currentTerminalStatus: "running", | ||
| 68 | hasCurrentTerminalHydration: true, | ||
| 69 | }), | ||
| 70 | ).toEqual({ | ||
| 71 | kind: "idle", | ||
| 72 | }); | ||
| 73 | }); | ||
| 74 | |||
| 75 | it("stays idle when the current running terminal is already hydrated in client state", () => { | ||
| 76 | expect( | ||
| 77 | resolveTerminalRouteBootstrap({ | ||
| 78 | hasThread: true, | ||
| 79 | hasWorkspaceRoot: true, | ||
| 80 | hasOpened: false, | ||
| 81 | requestedTerminalId: null, | ||
| 82 | currentTerminalId: "default", | ||
| 83 | runningTerminalId: "default", | ||
| 84 | currentTerminalStatus: "running", | ||
| 85 | hasCurrentTerminalHydration: true, | ||
| 86 | }), | ||
| 87 | ).toEqual({ | ||
| 88 | kind: "idle", | ||
| 89 | }); | ||
| 90 | }); | ||
| 91 | |||
| 92 | it("stays idle for explicit running terminal routes that already have hydrated output", () => { | ||
| 93 | expect( | ||
| 94 | resolveTerminalRouteBootstrap({ | ||
| 95 | hasThread: true, | ||
| 96 | hasWorkspaceRoot: true, | ||
| 97 | hasOpened: false, | ||
| 98 | requestedTerminalId: "term-2", | ||
| 99 | currentTerminalId: "term-2", | ||
| 100 | runningTerminalId: "term-2", | ||
| 101 | currentTerminalStatus: "running", | ||
| 102 | hasCurrentTerminalHydration: true, | ||
| 103 | }), | ||
| 104 | ).toEqual({ | ||
| 105 | kind: "idle", | ||
| 106 | }); | ||
| 107 | }); | ||
| 108 | }); | ||
apps/mobile/src/features/terminal/terminalRouteBootstrap.ts deleted-35| ... | @@ -1,35 +0,0 @@ | ||
| 1 | export function resolveTerminalRouteBootstrap(input: { | ||
| 2 | readonly hasThread: boolean; | ||
| 3 | readonly hasWorkspaceRoot: boolean; | ||
| 4 | readonly hasOpened: boolean; | ||
| 5 | readonly requestedTerminalId: string | null; | ||
| 6 | readonly currentTerminalId: string; | ||
| 7 | readonly runningTerminalId: string | null; | ||
| 8 | readonly currentTerminalStatus: "starting" | "running" | "exited" | "error" | "closed"; | ||
| 9 | /** True once the attach stream has populated scrollback (`buffer` non-empty), not merely metadata. */ | ||
| 10 | readonly hasCurrentTerminalHydration: boolean; | ||
| 11 | }): | ||
| 12 | | { readonly kind: "idle" } | ||
| 13 | | { readonly kind: "redirect"; readonly terminalId: string } | ||
| 14 | | { readonly kind: "open" } { | ||
| 15 | if (!input.hasThread || !input.hasWorkspaceRoot || input.hasOpened) { | ||
| 16 | return { kind: "idle" }; | ||
| 17 | } | ||
| 18 | |||
| 19 | if ( | ||
| 20 | input.requestedTerminalId === null && | ||
| 21 | input.runningTerminalId !== null && | ||
| 22 | input.runningTerminalId !== input.currentTerminalId | ||
| 23 | ) { | ||
| 24 | return { kind: "redirect", terminalId: input.runningTerminalId }; | ||
| 25 | } | ||
| 26 | |||
| 27 | if ( | ||
| 28 | (input.currentTerminalStatus === "running" || input.currentTerminalStatus === "starting") && | ||
| 29 | input.hasCurrentTerminalHydration | ||
| 30 | ) { | ||
| 31 | return { kind: "idle" }; | ||
| 32 | } | ||
| 33 | |||
| 34 | return { kind: "open" }; | ||
| 35 | } | ||
apps/server/integration/OrchestrationEngineHarness.integration.ts+2-2| ... | @@ -25,7 +25,7 @@ import * as Stream from "effect/Stream"; | ... | @@ -25,7 +25,7 @@ import * as Stream from "effect/Stream"; |
| 25 | import * as Tracer from "effect/Tracer"; | 25 | import * as Tracer from "effect/Tracer"; |
| 26 | 26 | ||
| 27 | import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts"; | 27 | import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts"; |
| 28 | import { TextGeneration, type TextGenerationShape } from "../src/textGeneration/TextGeneration.ts"; | 28 | import { TextGeneration } from "../src/textGeneration/TextGeneration.ts"; |
| 29 | import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; | 29 | import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; |
| 30 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; | 30 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; |
| 31 | import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; | 31 | import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; |
| ... | @@ -335,7 +335,7 @@ export const makeOrchestrationIntegrationHarness = ( | ... | @@ -335,7 +335,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 335 | const textGenerationLayer = Layer.succeed(TextGeneration, { | 335 | const textGenerationLayer = Layer.succeed(TextGeneration, { |
| 336 | generateBranchName: () => Effect.succeed({ branch: "update" }), | 336 | generateBranchName: () => Effect.succeed({ branch: "update" }), |
| 337 | generateThreadTitle: () => Effect.succeed({ title: "New thread" }), | 337 | generateThreadTitle: () => Effect.succeed({ title: "New thread" }), |
| 338 | } as unknown as TextGenerationShape); | 338 | } as unknown as TextGeneration["Service"]); |
| 339 | const providerCommandReactorLayer = ProviderCommandReactorLive.pipe( | 339 | const providerCommandReactorLayer = ProviderCommandReactorLive.pipe( |
| 340 | Layer.provideMerge(runtimeServicesLayer), | 340 | Layer.provideMerge(runtimeServicesLayer), |
| 341 | Layer.provideMerge(gitWorkflowLayer), | 341 | Layer.provideMerge(gitWorkflowLayer), |
apps/server/package.json-1| ... | @@ -25,7 +25,6 @@ | ... | @@ -25,7 +25,6 @@ |
| 25 | "@anthropic-ai/claude-agent-sdk": "^0.3.170", | 25 | "@anthropic-ai/claude-agent-sdk": "^0.3.170", |
| 26 | "@effect/platform-bun": "catalog:", | 26 | "@effect/platform-bun": "catalog:", |
| 27 | "@effect/platform-node": "catalog:", | 27 | "@effect/platform-node": "catalog:", |
| 28 | "@effect/platform-node-shared": "catalog:", | ||
| 29 | "@effect/sql-sqlite-bun": "catalog:", | 28 | "@effect/sql-sqlite-bun": "catalog:", |
| 30 | "@ff-labs/fff-node": "0.9.4", | 29 | "@ff-labs/fff-node": "0.9.4", |
| 31 | "@opencode-ai/sdk": "^1.3.15", | 30 | "@opencode-ai/sdk": "^1.3.15", |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+3-3| ... | @@ -45,7 +45,7 @@ import { | ... | @@ -45,7 +45,7 @@ import { |
| 45 | type ProviderServiceShape, | 45 | type ProviderServiceShape, |
| 46 | } from "../../provider/Services/ProviderService.ts"; | 46 | } from "../../provider/Services/ProviderService.ts"; |
| 47 | import { makeProviderRegistryLayer } from "../../provider/testUtils/providerRegistryMock.ts"; | 47 | import { makeProviderRegistryLayer } from "../../provider/testUtils/providerRegistryMock.ts"; |
| 48 | import { TextGeneration, type TextGenerationShape } from "../../textGeneration/TextGeneration.ts"; | 48 | import { TextGeneration } from "../../textGeneration/TextGeneration.ts"; |
| 49 | import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; | 49 | import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; |
| 50 | import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; | 50 | import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; |
| 51 | import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; | 51 | import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; |
| ... | @@ -310,7 +310,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -310,7 +310,7 @@ describe("ProviderCommandReactor", () => { |
| 310 | pr: null, | 310 | pr: null, |
| 311 | }), | 311 | }), |
| 312 | ); | 312 | ); |
| 313 | const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>((_) => | 313 | const generateBranchName = vi.fn<TextGeneration["Service"]["generateBranchName"]>((_) => |
| 314 | Effect.fail( | 314 | Effect.fail( |
| 315 | new TextGenerationError({ | 315 | new TextGenerationError({ |
| 316 | operation: "generateBranchName", | 316 | operation: "generateBranchName", |
| ... | @@ -318,7 +318,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -318,7 +318,7 @@ describe("ProviderCommandReactor", () => { |
| 318 | }), | 318 | }), |
| 319 | ), | 319 | ), |
| 320 | ); | 320 | ); |
| 321 | const generateThreadTitle = vi.fn<TextGenerationShape["generateThreadTitle"]>((_) => | 321 | const generateThreadTitle = vi.fn<TextGeneration["Service"]["generateThreadTitle"]>((_) => |
| 322 | Effect.fail( | 322 | Effect.fail( |
| 323 | new TextGenerationError({ | 323 | new TextGenerationError({ |
| 324 | operation: "generateThreadTitle", | 324 | operation: "generateThreadTitle", |
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts-13| ... | @@ -96,11 +96,6 @@ const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = { | ... | @@ -96,11 +96,6 @@ const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = { |
| 96 | streamEvents: Stream.empty, | 96 | streamEvents: Stream.empty, |
| 97 | }; | 97 | }; |
| 98 | 98 | ||
| 99 | // ProviderAdapterRegistryLive is now a facade over ProviderInstanceRegistry — | ||
| 100 | // it walks `listInstances` once at boot and surfaces the default-instance | ||
| 101 | // adapter keyed by its driver kind. To test the facade we supply four fake | ||
| 102 | // instances whose `instanceId === defaultInstanceIdForDriver(driverKind)` so | ||
| 103 | // they pass the default-instance filter. | ||
| 104 | const makeFakeInstance = ( | 99 | const makeFakeInstance = ( |
| 105 | driverKindString: "codex" | "claudeAgent" | "cursor" | "opencode", | 100 | driverKindString: "codex" | "claudeAgent" | "cursor" | "opencode", |
| 106 | adapter: ProviderInstance["adapter"], | 101 | adapter: ProviderInstance["adapter"], |
| ... | @@ -184,13 +179,5 @@ it.layer(layer)("ProviderAdapterRegistryLive", (it) => { | ... | @@ -184,13 +179,5 @@ it.layer(layer)("ProviderAdapterRegistryLive", (it) => { |
| 184 | defaultInstanceIdForDriver(OPENCODE_DRIVER), | 179 | defaultInstanceIdForDriver(OPENCODE_DRIVER), |
| 185 | defaultInstanceIdForDriver(CURSOR_DRIVER), | 180 | defaultInstanceIdForDriver(CURSOR_DRIVER), |
| 186 | ]); | 181 | ]); |
| 187 | |||
| 188 | const providers = yield* registry.listProviders(); | ||
| 189 | assert.deepStrictEqual(providers, [ | ||
| 190 | CODEX_DRIVER, | ||
| 191 | CLAUDE_AGENT_DRIVER, | ||
| 192 | OPENCODE_DRIVER, | ||
| 193 | CURSOR_DRIVER, | ||
| 194 | ]); | ||
| 195 | })); | 182 | })); |
| 196 | }); | 183 | }); |
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+1-26| ... | @@ -15,11 +15,7 @@ | ... | @@ -15,11 +15,7 @@ |
| 15 | * | 15 | * |
| 16 | * @module ProviderAdapterRegistryLive | 16 | * @module ProviderAdapterRegistryLive |
| 17 | */ | 17 | */ |
| 18 | import { | 18 | import { ProviderInstanceId } from "@t3tools/contracts"; |
| 19 | defaultInstanceIdForDriver, | ||
| 20 | ProviderInstanceId, | ||
| 21 | type ProviderDriverKind, | ||
| 22 | } from "@t3tools/contracts"; | ||
| 23 | import * as Effect from "effect/Effect"; | 19 | import * as Effect from "effect/Effect"; |
| 24 | import * as Layer from "effect/Layer"; | 20 | import * as Layer from "effect/Layer"; |
| 25 | 21 | ||
| ... | @@ -71,31 +67,10 @@ const makeProviderAdapterRegistry = Effect.fn("makeProviderAdapterRegistry")(fun | ... | @@ -71,31 +67,10 @@ const makeProviderAdapterRegistry = Effect.fn("makeProviderAdapterRegistry")(fun |
| 71 | Effect.map((instances) => instances.map((instance) => instance.instanceId)), | 67 | Effect.map((instances) => instances.map((instance) => instance.instanceId)), |
| 72 | ); | 68 | ); |
| 73 | 69 | ||
| 74 | const listProviders: ProviderAdapterRegistryShape["listProviders"] = () => | ||
| 75 | registry.listInstances.pipe( | ||
| 76 | Effect.map((instances) => { | ||
| 77 | const kinds = new Set<ProviderDriverKind>(); | ||
| 78 | for (const instance of instances) { | ||
| 79 | const defaultId = defaultInstanceIdForDriver(instance.driverKind); | ||
| 80 | if (instance.instanceId === defaultId) { | ||
| 81 | // Only the default-instance rows show up through the legacy | ||
| 82 | // shim — custom instances like `codex_personal` have no | ||
| 83 | // `ProviderDriverKind` equivalent. | ||
| 84 | kinds.add(instance.driverKind); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | return Array.from(kinds); | ||
| 88 | }), | ||
| 89 | ); | ||
| 90 | |||
| 91 | return { | 70 | return { |
| 92 | getByInstance, | 71 | getByInstance, |
| 93 | getInstanceInfo, | 72 | getInstanceInfo, |
| 94 | listInstances, | 73 | listInstances, |
| 95 | listProviders, | ||
| 96 | // Proxy directly — the facade has no state of its own; the instance | ||
| 97 | // registry already coalesces adds/removes/rebuilds into one emission. | ||
| 98 | streamChanges: registry.streamChanges, | ||
| 99 | subscribeChanges: registry.subscribeChanges, | 74 | subscribeChanges: registry.subscribeChanges, |
| 100 | } satisfies ProviderAdapterRegistryShape; | 75 | } satisfies ProviderAdapterRegistryShape; |
| 101 | }); | 76 | }); |
apps/server/src/provider/Layers/ProviderService.test.ts-4| ... | @@ -514,8 +514,6 @@ it.effect( | ... | @@ -514,8 +514,6 @@ it.effect( |
| 514 | }) | 514 | }) |
| 515 | : Effect.fail(unsupported()), | 515 | : Effect.fail(unsupported()), |
| 516 | listInstances: () => Effect.succeed([instanceId]), | 516 | listInstances: () => Effect.succeed([instanceId]), |
| 517 | listProviders: () => Effect.succeed([driverKind] as const), | ||
| 518 | streamChanges: Stream.empty, | ||
| 519 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => | 517 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => |
| 520 | PubSub.subscribe(pubsub), | 518 | PubSub.subscribe(pubsub), |
| 521 | ), | 519 | ), |
| ... | @@ -594,8 +592,6 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance | ... | @@ -594,8 +592,6 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance |
| 594 | }) | 592 | }) |
| 595 | : Effect.fail(unsupported()), | 593 | : Effect.fail(unsupported()), |
| 596 | listInstances: () => Effect.succeed([instanceId]), | 594 | listInstances: () => Effect.succeed([instanceId]), |
| 597 | listProviders: () => Effect.succeed([CODEX_DRIVER] as const), | ||
| 598 | streamChanges: Stream.empty, | ||
| 599 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => | 595 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => |
| 600 | PubSub.subscribe(pubsub), | 596 | PubSub.subscribe(pubsub), |
| 601 | ), | 597 | ), |
apps/server/src/provider/Services/ProviderAdapterRegistry.ts+3-35| ... | @@ -1,21 +1,9 @@ | ... | @@ -1,21 +1,9 @@ |
| 1 | /** | 1 | /** |
| 2 | * ProviderAdapterRegistry - Lookup boundary for provider adapter implementations. | 2 | * ProviderAdapterRegistry - Lookup boundary for provider adapter implementations. |
| 3 | * | 3 | * |
| 4 | * Maps a `ProviderInstanceId` (the new per-instance routing key) or a | 4 | * Maps a `ProviderInstanceId` to its provider adapter. `ProviderService` uses |
| 5 | * `ProviderDriverKind` (legacy single-instance-per-driver key) to the concrete | 5 | * this registry with `ProviderSessionDirectory`. The registry does not own |
| 6 | * adapter service (Codex, Claude, etc). It does not own session lifecycle | 6 | * session lifecycle or routing rules. |
| 7 | * or routing rules; `ProviderService` uses this registry together with | ||
| 8 | * `ProviderSessionDirectory`. | ||
| 9 | * | ||
| 10 | * During the driver/instance migration this tag exposes both flavours: | ||
| 11 | * | ||
| 12 | * - `getByInstance` / `listInstances` — new per-instance routing. Callers | ||
| 13 | * that already know an `instanceId` (threads, sessions, events) | ||
| 14 | * should prefer these. | ||
| 15 | * (`defaultInstanceIdForDriver(kind) === kind`), matching the pre-Slice-D | ||
| 16 | * behaviour. New code should not grow additional callers of the kind-keyed | ||
| 17 | * methods; they exist so the settings UI, WS refresh RPC, and a handful | ||
| 18 | * of legacy persisted rows can still be routed during the rollout. | ||
| 19 | * | 7 | * |
| 20 | * @module ProviderAdapterRegistry | 8 | * @module ProviderAdapterRegistry |
| 21 | */ | 9 | */ |
| ... | @@ -24,7 +12,6 @@ import * as Context from "effect/Context"; | ... | @@ -24,7 +12,6 @@ import * as Context from "effect/Context"; |
| 24 | import type * as Effect from "effect/Effect"; | 12 | import type * as Effect from "effect/Effect"; |
| 25 | import type * as PubSub from "effect/PubSub"; | 13 | import type * as PubSub from "effect/PubSub"; |
| 26 | import type * as Scope from "effect/Scope"; | 14 | import type * as Scope from "effect/Scope"; |
| 27 | import type * as Stream from "effect/Stream"; | ||
| 28 | 15 | ||
| 29 | import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts"; | 16 | import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts"; |
| 30 | import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; | 17 | import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; |
| ... | @@ -64,25 +51,6 @@ export interface ProviderAdapterRegistryShape { | ... | @@ -64,25 +51,6 @@ export interface ProviderAdapterRegistryShape { |
| 64 | */ | 51 | */ |
| 65 | readonly listInstances: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>; | 52 | readonly listInstances: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>; |
| 66 | 53 | ||
| 67 | /** | ||
| 68 | * Legacy: list provider kinds whose default instance is currently | ||
| 69 | * registered. | ||
| 70 | * | ||
| 71 | * @deprecated Prefer `listInstances`. Retained for migration-era call | ||
| 72 | * sites that iterate providers to build UI/metrics. | ||
| 73 | */ | ||
| 74 | readonly listProviders: () => Effect.Effect<ReadonlyArray<ProviderDriverKind>>; | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Change notification stream mirroring `ProviderInstanceRegistry.streamChanges`. | ||
| 78 | * Emits one `void` tick whenever the set of live instances changes | ||
| 79 | * (instance added, removed, or rebuilt after a settings edit). Consumers | ||
| 80 | * that fan out `adapter.streamEvents` per instance — e.g. `ProviderService`'s | ||
| 81 | * runtime event bus — re-pull `listInstances` on each tick and fork new | ||
| 82 | * subscriptions for instances they haven't seen yet. | ||
| 83 | */ | ||
| 84 | readonly streamChanges: Stream.Stream<void>; | ||
| 85 | |||
| 86 | /** | 54 | /** |
| 87 | * Acquire a change subscription synchronously in the caller's current fiber. | 55 | * Acquire a change subscription synchronously in the caller's current fiber. |
| 88 | * Consumers that must avoid missing a publish between initial reconciliation | 56 | * Consumers that must avoid missing a publish between initial reconciliation |
apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts+2-27| ... | @@ -2,15 +2,6 @@ | ... | @@ -2,15 +2,6 @@ |
| 2 | * Test helpers for constructing a `ProviderAdapterRegistryShape` mock from a | 2 | * Test helpers for constructing a `ProviderAdapterRegistryShape` mock from a |
| 3 | * kind-keyed adapter map. | 3 | * kind-keyed adapter map. |
| 4 | * | 4 | * |
| 5 | * Tests historically assembled a `registry` object with only `getByProvider` | ||
| 6 | * + `listProviders` populated. Slice D grew the shape with `getByInstance` | ||
| 7 | * and `listInstances`; this helper fills both in from a single kind-keyed | ||
| 8 | * input so individual fixtures can stay concise. | ||
| 9 | * | ||
| 10 | * Non-default instance ids (e.g. `codex_personal`) are not addressable via | ||
| 11 | * the shim returned here — the legacy test fixtures only ever had | ||
| 12 | * single-instance-per-driver data anyway. | ||
| 13 | * | ||
| 14 | * @module provider/testUtils/providerAdapterRegistryMock | 5 | * @module provider/testUtils/providerAdapterRegistryMock |
| 15 | */ | 6 | */ |
| 16 | import { | 7 | import { |
| ... | @@ -20,9 +11,6 @@ import { | ... | @@ -20,9 +11,6 @@ import { |
| 20 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 21 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 22 | import * as PubSub from "effect/PubSub"; | 13 | import * as PubSub from "effect/PubSub"; |
| 23 | import * as Record from "effect/Record"; | ||
| 24 | import * as Result from "effect/Result"; | ||
| 25 | import * as Stream from "effect/Stream"; | ||
| 26 | 14 | ||
| 27 | import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts"; | 15 | import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts"; |
| 28 | import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; | 16 | import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; |
| ... | @@ -34,9 +22,8 @@ export type KindAdapterMap = Partial< | ... | @@ -34,9 +22,8 @@ export type KindAdapterMap = Partial< |
| 34 | 22 | ||
| 35 | /** | 23 | /** |
| 36 | * Build a `ProviderAdapterRegistryShape` from a kind-keyed adapter map. | 24 | * Build a `ProviderAdapterRegistryShape` from a kind-keyed adapter map. |
| 37 | * Every adapter present in the map is addressable via both the legacy | 25 | * Every adapter present in the map is addressable through its default |
| 38 | * `getByProvider(kind)` path and the new `getByInstance(id)` path (where | 26 | * provider instance id. |
| 39 | * `id = defaultInstanceIdForDriver(kind)`). | ||
| 40 | */ | 27 | */ |
| 41 | export const makeAdapterRegistryMock = (adapters: KindAdapterMap): ProviderAdapterRegistryShape => { | 28 | export const makeAdapterRegistryMock = (adapters: KindAdapterMap): ProviderAdapterRegistryShape => { |
| 42 | const byInstanceId = new Map<ProviderInstanceId, ProviderAdapterShape<ProviderAdapterError>>(); | 29 | const byInstanceId = new Map<ProviderInstanceId, ProviderAdapterShape<ProviderAdapterError>>(); |
| ... | @@ -80,18 +67,6 @@ export const makeAdapterRegistryMock = (adapters: KindAdapterMap): ProviderAdapt | ... | @@ -80,18 +67,6 @@ export const makeAdapterRegistryMock = (adapters: KindAdapterMap): ProviderAdapt |
| 80 | }); | 67 | }); |
| 81 | }, | 68 | }, |
| 82 | listInstances: () => Effect.succeed(Array.from(byInstanceId.keys())), | 69 | listInstances: () => Effect.succeed(Array.from(byInstanceId.keys())), |
| 83 | listProviders: () => | ||
| 84 | Effect.succeed( | ||
| 85 | Record.keys( | ||
| 86 | Record.filterMap(adapters, (adapter, kind) => | ||
| 87 | adapter !== undefined ? Result.succeed(kind) : Result.failVoid, | ||
| 88 | ), | ||
| 89 | ), | ||
| 90 | ), | ||
| 91 | // Static test fixtures don't reload; an empty stream is enough to | ||
| 92 | // satisfy the shape. Tests exercising hot-reload build their own | ||
| 93 | // stream via the real `ProviderInstanceRegistry`. | ||
| 94 | streamChanges: Stream.empty, | ||
| 95 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => | 70 | subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) => |
| 96 | PubSub.subscribe(pubsub), | 71 | PubSub.subscribe(pubsub), |
| 97 | ), | 72 | ), |
apps/server/src/textGeneration/TextGeneration.ts-12| ... | @@ -73,15 +73,6 @@ export interface ThreadTitleGenerationResult { | ... | @@ -73,15 +73,6 @@ export interface ThreadTitleGenerationResult { |
| 73 | title: string; | 73 | title: string; |
| 74 | } | 74 | } |
| 75 | 75 | ||
| 76 | export interface TextGenerationService { | ||
| 77 | generateCommitMessage( | ||
| 78 | input: CommitMessageGenerationInput, | ||
| 79 | ): Promise<CommitMessageGenerationResult>; | ||
| 80 | generatePrContent(input: PrContentGenerationInput): Promise<PrContentGenerationResult>; | ||
| 81 | generateBranchName(input: BranchNameGenerationInput): Promise<BranchNameGenerationResult>; | ||
| 82 | generateThreadTitle(input: ThreadTitleGenerationInput): Promise<ThreadTitleGenerationResult>; | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | 76 | /** |
| 86 | * TextGeneration - Service tag for commit and change request text generation. | 77 | * TextGeneration - Service tag for commit and change request text generation. |
| 87 | */ | 78 | */ |
| ... | @@ -116,9 +107,6 @@ export class TextGeneration extends Context.Service< | ... | @@ -116,9 +107,6 @@ export class TextGeneration extends Context.Service< |
| 116 | } | 107 | } |
| 117 | >()("t3/textGeneration/TextGeneration") {} | 108 | >()("t3/textGeneration/TextGeneration") {} |
| 118 | 109 | ||
| 119 | /** @deprecated Use `TextGeneration["Service"]`. */ | ||
| 120 | export type TextGenerationShape = TextGeneration["Service"]; | ||
| 121 | |||
| 122 | type TextGenerationOp = | 110 | type TextGenerationOp = |
| 123 | | "generateCommitMessage" | 111 | | "generateCommitMessage" |
| 124 | | "generatePrContent" | 112 | | "generatePrContent" |
apps/web/src/cloud/linkEnvironment.ts-45| ... | @@ -16,7 +16,6 @@ import { | ... | @@ -16,7 +16,6 @@ import { |
| 16 | WS_METHODS, | 16 | WS_METHODS, |
| 17 | } from "@t3tools/contracts"; | 17 | } from "@t3tools/contracts"; |
| 18 | import { | 18 | import { |
| 19 | type RelayClientDeviceRecord, | ||
| 20 | type RelayClientEnvironmentRecord, | 19 | type RelayClientEnvironmentRecord, |
| 21 | type RelayEnvironmentLinkResponse, | 20 | type RelayEnvironmentLinkResponse, |
| 22 | type RelayManagedEndpointProviderKind, | 21 | type RelayManagedEndpointProviderKind, |
| ... | @@ -26,10 +25,6 @@ import { request, runStream } from "@t3tools/client-runtime/rpc"; | ... | @@ -26,10 +25,6 @@ import { request, runStream } from "@t3tools/client-runtime/rpc"; |
| 26 | import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; | 25 | import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; |
| 27 | import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay"; | 26 | import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay"; |
| 28 | 27 | ||
| 29 | import { | ||
| 30 | readPrimaryEnvironmentDescriptor, | ||
| 31 | readPrimaryEnvironmentTarget, | ||
| 32 | } from "../environments/primary"; | ||
| 33 | import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer"; | 28 | import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer"; |
| 34 | import { resolveCloudPublicConfig } from "./publicConfig"; | 29 | import { resolveCloudPublicConfig } from "./publicConfig"; |
| 35 | import { | 30 | import { |
| ... | @@ -215,20 +210,6 @@ export function collectCloudLinkTargets(input: { | ... | @@ -215,20 +210,6 @@ export function collectCloudLinkTargets(input: { |
| 215 | return [...byId.values()]; | 210 | return [...byId.values()]; |
| 216 | } | 211 | } |
| 217 | 212 | ||
| 218 | export function readPrimaryCloudLinkTarget(): CloudLinkTarget | null { | ||
| 219 | const descriptor = readPrimaryEnvironmentDescriptor(); | ||
| 220 | const target = readPrimaryEnvironmentTarget(); | ||
| 221 | if (!descriptor || !target) { | ||
| 222 | return null; | ||
| 223 | } | ||
| 224 | return { | ||
| 225 | environmentId: descriptor.environmentId, | ||
| 226 | label: descriptor.label, | ||
| 227 | httpBaseUrl: target.target.httpBaseUrl, | ||
| 228 | wsBaseUrl: target.target.wsBaseUrl, | ||
| 229 | }; | ||
| 230 | } | ||
| 231 | |||
| 232 | export function listManagedCloudEnvironments(input: { | 213 | export function listManagedCloudEnvironments(input: { |
| 233 | readonly clerkToken: string; | 214 | readonly clerkToken: string; |
| 234 | }): Effect.Effect< | 215 | }): Effect.Effect< |
| ... | @@ -260,32 +241,6 @@ export function listManagedCloudEnvironments(input: { | ... | @@ -260,32 +241,6 @@ export function listManagedCloudEnvironments(input: { |
| 260 | }); | 241 | }); |
| 261 | } | 242 | } |
| 262 | 243 | ||
| 263 | export function listCloudDevices(input: { | ||
| 264 | readonly clerkToken: string; | ||
| 265 | }): Effect.Effect< | ||
| 266 | ReadonlyArray<RelayClientDeviceRecord>, | ||
| 267 | CloudEnvironmentLinkError, | ||
| 268 | ManagedRelay.ManagedRelayClient | ||
| 269 | > { | ||
| 270 | return Effect.gen(function* () { | ||
| 271 | if (!relayUrl()) { | ||
| 272 | return yield* new CloudEnvironmentLinkError({ | ||
| 273 | message: "T3CODE_RELAY_URL is not configured.", | ||
| 274 | }); | ||
| 275 | } | ||
| 276 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | ||
| 277 | return yield* relayClient.listDevices({ clerkToken: input.clerkToken }).pipe( | ||
| 278 | Effect.mapError( | ||
| 279 | (cause) => | ||
| 280 | new CloudEnvironmentLinkError({ | ||
| 281 | message: "Could not list cloud devices.", | ||
| 282 | cause, | ||
| 283 | }), | ||
| 284 | ), | ||
| 285 | ); | ||
| 286 | }); | ||
| 287 | } | ||
| 288 | |||
| 289 | export function readPrimaryCloudLinkState(input: { | 244 | export function readPrimaryCloudLinkState(input: { |
| 290 | readonly target: CloudLinkTarget; | 245 | readonly target: CloudLinkTarget; |
| 291 | }): Effect.Effect<CloudLinkState | null, CloudEnvironmentLinkError, HttpClient.HttpClient> { | 246 | }): Effect.Effect<CloudLinkState | null, CloudEnvironmentLinkError, HttpClient.HttpClient> { |
apps/web/src/components/DiffPanelShell.tsx-14| ... | @@ -46,20 +46,6 @@ export function DiffPanelShell(props: { | ... | @@ -46,20 +46,6 @@ export function DiffPanelShell(props: { |
| 46 | ); | 46 | ); |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | export function DiffPanelHeaderSkeleton() { | ||
| 50 | return ( | ||
| 51 | <> | ||
| 52 | <div className="min-w-0 flex-1"> | ||
| 53 | <Skeleton className="h-8 w-32 rounded-lg" /> | ||
| 54 | </div> | ||
| 55 | <div className="flex shrink-0 gap-1"> | ||
| 56 | <Skeleton className="size-7 rounded-md" /> | ||
| 57 | <Skeleton className="size-7 rounded-md" /> | ||
| 58 | </div> | ||
| 59 | </> | ||
| 60 | ); | ||
| 61 | } | ||
| 62 | |||
| 63 | function DiffFileHeaderSkeleton({ titleClassName }: { titleClassName: string }) { | 49 | function DiffFileHeaderSkeleton({ titleClassName }: { titleClassName: string }) { |
| 64 | return ( | 50 | return ( |
| 65 | <div className="flex h-8 items-center gap-2 px-2 pr-3"> | 51 | <div className="flex h-8 items-center gap-2 px-2 pr-3"> |
apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts-4| ... | @@ -202,10 +202,6 @@ export function providerUpdateNotificationKey( | ... | @@ -202,10 +202,6 @@ export function providerUpdateNotificationKey( |
| 202 | return parts.length > 0 ? parts.join("|") : null; | 202 | return parts.length > 0 ? parts.join("|") : null; |
| 203 | } | 203 | } |
| 204 | 204 | ||
| 205 | export function providerUpdateCandidateKey(provider: ProviderUpdateCandidate): string { | ||
| 206 | return providerUpdateNotificationKey([provider])!; | ||
| 207 | } | ||
| 208 | |||
| 209 | export function formatProviderList(providers: ReadonlyArray<Pick<ServerProvider, "driver">>) { | 205 | export function formatProviderList(providers: ReadonlyArray<Pick<ServerProvider, "driver">>) { |
| 210 | const names = providers.map( | 206 | const names = providers.map( |
| 211 | (provider) => PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver, | 207 | (provider) => PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver, |
apps/web/src/components/chat/ChangedFilesTree.test.tsx-11| ... | @@ -22,17 +22,6 @@ describe("ChangedFilesCard", () => { | ... | @@ -22,17 +22,6 @@ describe("ChangedFilesCard", () => { |
| 22 | 22 | ||
| 23 | expect(markup).toContain('data-changed-files-state="expanded"'); | 23 | expect(markup).toContain('data-changed-files-state="expanded"'); |
| 24 | expect(markup).toContain('aria-expanded="true"'); | 24 | expect(markup).toContain('aria-expanded="true"'); |
| 25 | expect(markup).toContain("whitespace-nowrap"); | ||
| 26 | expect(markup).toContain('class="group flex min-w-0 flex-1 items-center rounded-xl'); | ||
| 27 | expect(markup).not.toMatch(/class="group flex min-w-0 flex-1 items-center[^"]*overflow-hidden/); | ||
| 28 | expect(markup).toContain('class="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden"'); | ||
| 29 | expect(markup).toContain('class="flex shrink-0 items-center gap-1 whitespace-nowrap'); | ||
| 30 | expect(markup).toContain('class="ml-1 hidden min-w-0 flex-1 truncate'); | ||
| 31 | expect(markup).toContain("@[24rem]/changed-files:inline"); | ||
| 32 | expect(markup).not.toContain("sm:inline"); | ||
| 33 | expect(markup).toContain('class="flex shrink-0 items-center gap-1.5 pr-1"'); | ||
| 34 | expect(markup).toContain("!size-[22px]"); | ||
| 35 | expect(markup).toContain("size-3"); | ||
| 36 | expect(markup).toContain('aria-label="Collapse all folders"'); | 25 | expect(markup).toContain('aria-label="Collapse all folders"'); |
| 37 | expect(markup).toContain('aria-label="Open diff"'); | 26 | expect(markup).toContain('aria-label="Open diff"'); |
| 38 | expect(markup).toContain('role="group" aria-label="2 additions, 1 deletions"'); | 27 | expect(markup).toContain('role="group" aria-label="2 additions, 1 deletions"'); |
apps/web/src/components/chat/ComposerCommandMenu.test.tsx+3-36| ... | @@ -5,24 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; | ... | @@ -5,24 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; |
| 5 | import { ComposerCommandMenu } from "./ComposerCommandMenu"; | 5 | import { ComposerCommandMenu } from "./ComposerCommandMenu"; |
| 6 | 6 | ||
| 7 | describe("ComposerCommandMenu", () => { | 7 | describe("ComposerCommandMenu", () => { |
| 8 | it("renders slash-command results as an attached composer drawer", () => { | 8 | it("renders slash commands with their descriptions", () => { |
| 9 | const markup = renderToStaticMarkup( | ||
| 10 | <ComposerCommandMenu | ||
| 11 | items={[]} | ||
| 12 | resolvedTheme="dark" | ||
| 13 | isLoading={false} | ||
| 14 | triggerKind="slash-command" | ||
| 15 | activeItemId={null} | ||
| 16 | onHighlightedItemChange={() => {}} | ||
| 17 | onSelect={() => {}} | ||
| 18 | />, | ||
| 19 | ); | ||
| 20 | |||
| 21 | expect(markup).toContain('data-composer-command-drawer="true"'); | ||
| 22 | expect(markup).not.toContain("dropdown-glass"); | ||
| 23 | }); | ||
| 24 | |||
| 25 | it("renders commands without a category heading or invented icons", () => { | ||
| 26 | const markup = renderToStaticMarkup( | 9 | const markup = renderToStaticMarkup( |
| 27 | <ComposerCommandMenu | 10 | <ComposerCommandMenu |
| 28 | items={[ | 11 | items={[ |
| ... | @@ -45,16 +28,9 @@ describe("ComposerCommandMenu", () => { | ... | @@ -45,16 +28,9 @@ describe("ComposerCommandMenu", () => { |
| 45 | 28 | ||
| 46 | expect(markup).toContain("/model"); | 29 | expect(markup).toContain("/model"); |
| 47 | expect(markup).toContain("Switch response model for this thread"); | 30 | expect(markup).toContain("Switch response model for this thread"); |
| 48 | expect(markup).not.toContain("Built-in"); | ||
| 49 | expect(markup).not.toContain("<svg"); | ||
| 50 | expect(markup).toContain("font-sans text-xs font-medium"); | ||
| 51 | expect(markup).not.toContain("font-mono"); | ||
| 52 | expect(markup).not.toContain("grid-cols-"); | ||
| 53 | expect(markup).toContain("max-w-[45%]"); | ||
| 54 | expect(markup).toContain("text-left"); | ||
| 55 | }); | 31 | }); |
| 56 | 32 | ||
| 57 | it("renders the skill source icon inside its badge", () => { | 33 | it("shows the app source for an app skill", () => { |
| 58 | const markup = renderToStaticMarkup( | 34 | const markup = renderToStaticMarkup( |
| 59 | <ComposerCommandMenu | 35 | <ComposerCommandMenu |
| 60 | items={[ | 36 | items={[ |
| ... | @@ -85,18 +61,10 @@ describe("ComposerCommandMenu", () => { | ... | @@ -85,18 +61,10 @@ describe("ComposerCommandMenu", () => { |
| 85 | expect(markup).toContain('data-slot="badge"'); | 61 | expect(markup).toContain('data-slot="badge"'); |
| 86 | expect(markup).toContain(">App Skill</span>"); | 62 | expect(markup).toContain(">App Skill</span>"); |
| 87 | expect(markup).toContain("Open and control the in-app browser"); | 63 | expect(markup).toContain("Open and control the in-app browser"); |
| 88 | expect(markup).toContain("max-w-[48ch]"); | ||
| 89 | expect(markup).toContain("text-secondary-label text-xs"); | ||
| 90 | expect(markup).toContain("ms-auto"); | ||
| 91 | expect(markup).toContain("text-current"); | ||
| 92 | expect(markup.indexOf("Open and control the in-app browser")).toBeLessThan( | ||
| 93 | markup.indexOf(">App Skill</span>"), | ||
| 94 | ); | ||
| 95 | expect(markup).toContain("<svg"); | 64 | expect(markup).toContain("<svg"); |
| 96 | expect(markup.indexOf('data-slot="badge"')).toBeLessThan(markup.indexOf("<svg")); | ||
| 97 | }); | 65 | }); |
| 98 | 66 | ||
| 99 | it("keeps slash skills aligned with the source icon inside the badge", () => { | 67 | it("shows the repo source for a slash skill", () => { |
| 100 | const markup = renderToStaticMarkup( | 68 | const markup = renderToStaticMarkup( |
| 101 | <ComposerCommandMenu | 69 | <ComposerCommandMenu |
| 102 | items={[ | 70 | items={[ |
| ... | @@ -129,6 +97,5 @@ describe("ComposerCommandMenu", () => { | ... | @@ -129,6 +97,5 @@ describe("ComposerCommandMenu", () => { |
| 129 | expect(markup).toContain("lucide-folder"); | 97 | expect(markup).toContain("lucide-folder"); |
| 130 | expect(markup).toContain(">Repo</span>"); | 98 | expect(markup).toContain(">Repo</span>"); |
| 131 | expect(markup).toContain("Find the right skill or workflow"); | 99 | expect(markup).toContain("Find the right skill or workflow"); |
| 132 | expect(markup).not.toContain("font-medium text-secondary-label"); | ||
| 133 | }); | 100 | }); |
| 134 | }); | 101 | }); |
apps/web/src/components/chat/ComposerPrimaryActions.test.tsx+1-32| ... | @@ -44,27 +44,6 @@ function renderPendingActions(isRunning: boolean) { | ... | @@ -44,27 +44,6 @@ function renderPendingActions(isRunning: boolean) { |
| 44 | ); | 44 | ); |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | function renderStandaloneStop() { | ||
| 48 | return renderToStaticMarkup( | ||
| 49 | createElement(ComposerPrimaryActions, { | ||
| 50 | compact: true, | ||
| 51 | pendingAction: null, | ||
| 52 | isRunning: true, | ||
| 53 | showPlanFollowUpPrompt: false, | ||
| 54 | promptHasText: false, | ||
| 55 | isSendBusy: false, | ||
| 56 | sendDisabledReason: null, | ||
| 57 | isConnecting: false, | ||
| 58 | isEnvironmentUnavailable: false, | ||
| 59 | isPreparingWorktree: false, | ||
| 60 | hasSendableContent: false, | ||
| 61 | onPreviousPendingQuestion: () => {}, | ||
| 62 | onInterrupt: () => {}, | ||
| 63 | onImplementPlanInNewThread: () => {}, | ||
| 64 | }), | ||
| 65 | ); | ||
| 66 | } | ||
| 67 | |||
| 68 | function renderRunningActions(showSendWhileRunning: boolean, hasSendableContent: boolean) { | 47 | function renderRunningActions(showSendWhileRunning: boolean, hasSendableContent: boolean) { |
| 69 | return renderToStaticMarkup( | 48 | return renderToStaticMarkup( |
| 70 | createElement(ComposerPrimaryActions, { | 49 | createElement(ComposerPrimaryActions, { |
| ... | @@ -219,12 +198,6 @@ describe("ComposerPrimaryActions", () => { | ... | @@ -219,12 +198,6 @@ describe("ComposerPrimaryActions", () => { |
| 219 | expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"'); | 198 | expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"'); |
| 220 | }); | 199 | }); |
| 221 | 200 | ||
| 222 | it("matches the small pending action size without changing the standalone size", () => { | ||
| 223 | expect(renderPendingActions(true)).toContain("size-8 sm:size-7"); | ||
| 224 | expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8"); | ||
| 225 | expect(renderStandaloneStop()).not.toContain("sm:size-7"); | ||
| 226 | }); | ||
| 227 | |||
| 228 | it("renders stage artwork inside the send button when artwork identification is active", () => { | 201 | it("renders stage artwork inside the send button when artwork identification is active", () => { |
| 229 | stageArtworkState.mode = "artwork"; | 202 | stageArtworkState.mode = "artwork"; |
| 230 | stageArtworkState.variant = "nightly"; | 203 | stageArtworkState.variant = "nightly"; |
| ... | @@ -232,17 +205,14 @@ describe("ComposerPrimaryActions", () => { | ... | @@ -232,17 +205,14 @@ describe("ComposerPrimaryActions", () => { |
| 232 | const markup = renderSendButton(); | 205 | const markup = renderSendButton(); |
| 233 | 206 | ||
| 234 | expect(markup).toContain("stage-nightly"); | 207 | expect(markup).toContain("stage-nightly"); |
| 235 | expect(markup).toContain("bg-transparent text-white"); | ||
| 236 | expect(markup).not.toContain("bg-message-action text-message-action-foreground"); | ||
| 237 | }); | 208 | }); |
| 238 | 209 | ||
| 239 | it("keeps the normal send-button fill when artwork identification is inactive", () => { | 210 | it("hides stage artwork when artwork identification is inactive", () => { |
| 240 | stageArtworkState.variant = "nightly"; | 211 | stageArtworkState.variant = "nightly"; |
| 241 | 212 | ||
| 242 | const markup = renderSendButton(); | 213 | const markup = renderSendButton(); |
| 243 | 214 | ||
| 244 | expect(markup).not.toContain("stage-nightly"); | 215 | expect(markup).not.toContain("stage-nightly"); |
| 245 | expect(markup).toContain("bg-message-action text-message-action-foreground"); | ||
| 246 | }); | 216 | }); |
| 247 | 217 | ||
| 248 | it("only renders stop while running when Enter-to-send is available", () => { | 218 | it("only renders stop while running when Enter-to-send is available", () => { |
| ... | @@ -258,7 +228,6 @@ describe("ComposerPrimaryActions", () => { | ... | @@ -258,7 +228,6 @@ describe("ComposerPrimaryActions", () => { |
| 258 | expect(markup).toContain('aria-label="Stop generation"'); | 228 | expect(markup).toContain('aria-label="Stop generation"'); |
| 259 | expect(markup).toContain('aria-label="Send message"'); | 229 | expect(markup).toContain('aria-label="Send message"'); |
| 260 | expect(markup).toContain('type="submit"'); | 230 | expect(markup).toContain('type="submit"'); |
| 261 | expect(markup).toContain("size-9 sm:size-8"); | ||
| 262 | }); | 231 | }); |
| 263 | 232 | ||
| 264 | it("keeps stop as the only action while running with an empty composer", () => { | 233 | it("keeps stop as the only action while running with an empty composer", () => { |
apps/web/src/components/chat/MessagesTimeline.test.tsx+1-126| ... | @@ -16,12 +16,7 @@ vi.mock("@legendapp/list/react", async () => { | ... | @@ -16,12 +16,7 @@ vi.mock("@legendapp/list/react", async () => { |
| 16 | ListFooterComponent?: ReactNode; | 16 | ListFooterComponent?: ReactNode; |
| 17 | anchoredEndSpace?: { | 17 | anchoredEndSpace?: { |
| 18 | anchorIndex: number; | 18 | anchorIndex: number; |
| 19 | anchorMaxSize?: number; | ||
| 20 | anchorOffset?: number; | ||
| 21 | onReady?: (info: { anchorIndex: number }) => void; | ||
| 22 | }; | 19 | }; |
| 23 | contentInsetEndAdjustment?: number; | ||
| 24 | className?: string; | ||
| 25 | maintainScrollAtEnd?: | 20 | maintainScrollAtEnd?: |
| 26 | | boolean | 21 | | boolean |
| 27 | | { | 22 | | { |
| ... | @@ -32,27 +27,12 @@ vi.mock("@legendapp/list/react", async () => { | ... | @@ -32,27 +27,12 @@ vi.mock("@legendapp/list/react", async () => { |
| 32 | layout?: boolean; | 27 | layout?: boolean; |
| 33 | }; | 28 | }; |
| 34 | }; | 29 | }; |
| 35 | maintainVisibleContentPosition?: | ||
| 36 | | boolean | ||
| 37 | | { | ||
| 38 | data?: boolean; | ||
| 39 | size?: boolean; | ||
| 40 | shouldRestorePosition?: (item: { id: string }) => boolean; | ||
| 41 | }; | ||
| 42 | ref?: Ref<LegendListRef>; | 30 | ref?: Ref<LegendListRef>; |
| 43 | }) => { | 31 | }) => { |
| 44 | if (props.anchoredEndSpace) { | ||
| 45 | props.anchoredEndSpace.onReady?.({ anchorIndex: props.anchoredEndSpace.anchorIndex }); | ||
| 46 | } | ||
| 47 | return ( | 32 | return ( |
| 48 | <div | 33 | <div |
| 49 | data-testid={legendListTestId} | 34 | data-testid={legendListTestId} |
| 50 | data-anchor-index={props.anchoredEndSpace?.anchorIndex} | 35 | data-anchor-index={props.anchoredEndSpace?.anchorIndex} |
| 51 | data-anchor-max-size={props.anchoredEndSpace?.anchorMaxSize} | ||
| 52 | data-anchor-offset={props.anchoredEndSpace?.anchorOffset} | ||
| 53 | data-anchor-on-ready={Boolean(props.anchoredEndSpace?.onReady)} | ||
| 54 | data-content-inset-end={props.contentInsetEndAdjustment} | ||
| 55 | data-class-name={props.className} | ||
| 56 | data-maintain-scroll-at-end={props.maintainScrollAtEnd ? "enabled" : undefined} | 36 | data-maintain-scroll-at-end={props.maintainScrollAtEnd ? "enabled" : undefined} |
| 57 | data-maintain-scroll-at-end-animated={ | 37 | data-maintain-scroll-at-end-animated={ |
| 58 | typeof props.maintainScrollAtEnd === "object" | 38 | typeof props.maintainScrollAtEnd === "object" |
| ... | @@ -74,26 +54,6 @@ vi.mock("@legendapp/list/react", async () => { | ... | @@ -74,26 +54,6 @@ vi.mock("@legendapp/list/react", async () => { |
| 74 | ? props.maintainScrollAtEnd.on?.layout | 54 | ? props.maintainScrollAtEnd.on?.layout |
| 75 | : undefined | 55 | : undefined |
| 76 | } | 56 | } |
| 77 | data-maintain-visible-content-position={ | ||
| 78 | typeof props.maintainVisibleContentPosition === "object" | ||
| 79 | ? "object" | ||
| 80 | : props.maintainVisibleContentPosition | ||
| 81 | } | ||
| 82 | data-maintain-visible-content-position-data={ | ||
| 83 | typeof props.maintainVisibleContentPosition === "object" | ||
| 84 | ? props.maintainVisibleContentPosition.data | ||
| 85 | : undefined | ||
| 86 | } | ||
| 87 | data-maintain-visible-content-position-size={ | ||
| 88 | typeof props.maintainVisibleContentPosition === "object" | ||
| 89 | ? props.maintainVisibleContentPosition.size | ||
| 90 | : undefined | ||
| 91 | } | ||
| 92 | data-maintain-visible-content-position-restore={ | ||
| 93 | typeof props.maintainVisibleContentPosition === "object" | ||
| 94 | ? Boolean(props.maintainVisibleContentPosition.shouldRestorePosition) | ||
| 95 | : undefined | ||
| 96 | } | ||
| 97 | > | 57 | > |
| 98 | {props.ListHeaderComponent} | 58 | {props.ListHeaderComponent} |
| 99 | {props.data.map((item) => ( | 59 | {props.data.map((item) => ( |
| ... | @@ -293,7 +253,7 @@ describe("MessagesTimeline", () => { | ... | @@ -293,7 +253,7 @@ describe("MessagesTimeline", () => { |
| 293 | expect(markup).toContain("codex-thread-1"); | 253 | expect(markup).toContain("codex-thread-1"); |
| 294 | }); | 254 | }); |
| 295 | 255 | ||
| 296 | it("renders the worked-for row at assistant response text size", () => { | 256 | it("renders elapsed time for a completed turn", () => { |
| 297 | const turnId = TurnId.make("turn-with-fold"); | 257 | const turnId = TurnId.make("turn-with-fold"); |
| 298 | const assistantEntry = buildAssistantTimelineEntry("Done."); | 258 | const assistantEntry = buildAssistantTimelineEntry("Done."); |
| 299 | const markup = renderToStaticMarkup( | 259 | const markup = renderToStaticMarkup( |
| ... | @@ -328,23 +288,6 @@ describe("MessagesTimeline", () => { | ... | @@ -328,23 +288,6 @@ describe("MessagesTimeline", () => { |
| 328 | ); | 288 | ); |
| 329 | 289 | ||
| 330 | expect(markup).toContain("Worked for 8.0s"); | 290 | expect(markup).toContain("Worked for 8.0s"); |
| 331 | expect(markup).toContain("px-1 text-sm leading-relaxed text-muted-foreground"); | ||
| 332 | }); | ||
| 333 | |||
| 334 | it("uses the larger leading inset only when the top fade is enabled", () => { | ||
| 335 | const timelineEntries = [buildUserTimelineEntry("Hello")]; | ||
| 336 | |||
| 337 | const compactMarkup = renderToStaticMarkup( | ||
| 338 | <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} />, | ||
| 339 | ); | ||
| 340 | const fadedMarkup = renderToStaticMarkup( | ||
| 341 | <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} topFadeEnabled />, | ||
| 342 | ); | ||
| 343 | |||
| 344 | expect(compactMarkup).toContain('class="h-3 sm:h-4"'); | ||
| 345 | expect(compactMarkup).not.toContain("topbar-scroll-fade"); | ||
| 346 | expect(fadedMarkup).toContain('class="h-[var(--workspace-titlebar-scroll-fade-height)]"'); | ||
| 347 | expect(fadedMarkup).toContain("topbar-scroll-fade"); | ||
| 348 | }); | 291 | }); |
| 349 | 292 | ||
| 350 | it("keeps assistant changed-files headers sticky below the thread header", () => { | 293 | it("keeps assistant changed-files headers sticky below the thread header", () => { |
| ... | @@ -491,74 +434,6 @@ describe("MessagesTimeline", () => { | ... | @@ -491,74 +434,6 @@ describe("MessagesTimeline", () => { |
| 491 | expect(resolveTimelineMinimapInteractiveWidth(40, true)).toBe("22rem"); | 434 | expect(resolveTimelineMinimapInteractiveWidth(40, true)).toBe("22rem"); |
| 492 | }); | 435 | }); |
| 493 | 436 | ||
| 494 | it("anchors the first user message using its measured height", () => { | ||
| 495 | const onAnchorReady = vi.fn(); | ||
| 496 | const firstEntry = { | ||
| 497 | ...buildUserTimelineEntry("First prompt."), | ||
| 498 | message: { | ||
| 499 | ...buildUserTimelineEntry("First prompt.").message, | ||
| 500 | attachments: [ | ||
| 501 | { | ||
| 502 | type: "image" as const, | ||
| 503 | id: "attachment-1", | ||
| 504 | name: "screenshot.png", | ||
| 505 | mimeType: "image/png", | ||
| 506 | sizeBytes: 1, | ||
| 507 | previewUrl: "data:image/png;base64,iVBORw0KGgo=", | ||
| 508 | }, | ||
| 509 | ], | ||
| 510 | }, | ||
| 511 | }; | ||
| 512 | const markup = renderToStaticMarkup( | ||
| 513 | <MessagesTimeline | ||
| 514 | {...buildProps()} | ||
| 515 | anchorMessageId={firstEntry.message.id} | ||
| 516 | onAnchorReady={onAnchorReady} | ||
| 517 | contentInsetEndAdjustment={144} | ||
| 518 | timelineEntries={[firstEntry]} | ||
| 519 | />, | ||
| 520 | ); | ||
| 521 | |||
| 522 | expect(markup).toContain('data-anchor-index="0"'); | ||
| 523 | expect(markup).toContain('data-anchor-offset="24"'); | ||
| 524 | expect(markup).toContain('data-anchor-on-ready="true"'); | ||
| 525 | expect(markup).not.toContain("data-anchor-max-size="); | ||
| 526 | expect(markup).toContain('data-content-inset-end="144"'); | ||
| 527 | expect(markup).toContain("[overflow-anchor:none]"); | ||
| 528 | expect(markup).not.toContain('data-maintain-scroll-at-end="enabled"'); | ||
| 529 | expect(markup).toContain('data-maintain-visible-content-position="object"'); | ||
| 530 | expect(markup).toContain('data-maintain-visible-content-position-data="true"'); | ||
| 531 | expect(markup).toContain('data-maintain-visible-content-position-size="true"'); | ||
| 532 | expect(markup).toContain('data-maintain-visible-content-position-restore="true"'); | ||
| 533 | expect(onAnchorReady).toHaveBeenCalledOnce(); | ||
| 534 | expect(onAnchorReady).toHaveBeenCalledWith(firstEntry.message.id, 0); | ||
| 535 | }); | ||
| 536 | |||
| 537 | it("does not reserve end space for a follow-up user message", () => { | ||
| 538 | const onAnchorReady = vi.fn(); | ||
| 539 | const firstEntry = buildUserTimelineEntry("First prompt."); | ||
| 540 | const secondEntry = { | ||
| 541 | ...buildUserTimelineEntry("Newest prompt."), | ||
| 542 | id: "entry-2", | ||
| 543 | message: { | ||
| 544 | ...buildUserTimelineEntry("Newest prompt.").message, | ||
| 545 | id: MessageId.make("message-2"), | ||
| 546 | }, | ||
| 547 | }; | ||
| 548 | const markup = renderToStaticMarkup( | ||
| 549 | <MessagesTimeline | ||
| 550 | {...buildProps()} | ||
| 551 | anchorMessageId={secondEntry.message.id} | ||
| 552 | onAnchorReady={onAnchorReady} | ||
| 553 | timelineEntries={[firstEntry, secondEntry]} | ||
| 554 | />, | ||
| 555 | ); | ||
| 556 | |||
| 557 | expect(markup).not.toContain("data-anchor-index="); | ||
| 558 | expect(markup).toContain('data-maintain-scroll-at-end="enabled"'); | ||
| 559 | expect(onAnchorReady).not.toHaveBeenCalled(); | ||
| 560 | }); | ||
| 561 | |||
| 562 | it("renders generic attachments as download links instead of image previews", () => { | 437 | it("renders generic attachments as download links instead of image previews", () => { |
| 563 | const entry = { | 438 | const entry = { |
| 564 | ...buildUserTimelineEntry("Read the report."), | 439 | ...buildUserTimelineEntry("Read the report."), |
apps/web/src/components/chat/providerIconUtils.ts-12| ... | @@ -1,6 +1,5 @@ | ... | @@ -1,6 +1,5 @@ |
| 1 | import { ProviderDriverKind } from "@t3tools/contracts"; | 1 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 2 | import { ClaudeAI, CursorIcon, GrokIcon, Icon, OpenAI, OpenCodeIcon } from "../Icons"; | 2 | import { ClaudeAI, CursorIcon, GrokIcon, Icon, OpenAI, OpenCodeIcon } from "../Icons"; |
| 3 | import { PROVIDER_OPTIONS } from "../../session-logic"; | ||
| 4 | 3 | ||
| 5 | export const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon>> = { | 4 | export const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon>> = { |
| 6 | [ProviderDriverKind.make("codex")]: OpenAI, | 5 | [ProviderDriverKind.make("codex")]: OpenAI, |
| ... | @@ -10,17 +9,6 @@ export const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon> | ... | @@ -10,17 +9,6 @@ export const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon> |
| 10 | [ProviderDriverKind.make("grok")]: GrokIcon, | 9 | [ProviderDriverKind.make("grok")]: GrokIcon, |
| 11 | }; | 10 | }; |
| 12 | 11 | ||
| 13 | function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { | ||
| 14 | value: ProviderDriverKind; | ||
| 15 | label: string; | ||
| 16 | available: true; | ||
| 17 | pickerSidebarBadge?: "new" | "soon"; | ||
| 18 | } { | ||
| 19 | return option.available; | ||
| 20 | } | ||
| 21 | |||
| 22 | export const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter(isAvailableProviderOption); | ||
| 23 | |||
| 24 | export type ModelEsque = { | 12 | export type ModelEsque = { |
| 25 | slug: string; | 13 | slug: string; |
| 26 | name: string; | 14 | name: string; |
apps/web/src/components/desktopUpdate.logic.ts-5| ... | @@ -125,11 +125,6 @@ export function shouldToastDesktopUpdateActionResult(result: DesktopUpdateAction | ... | @@ -125,11 +125,6 @@ export function shouldToastDesktopUpdateActionResult(result: DesktopUpdateAction |
| 125 | return getDesktopUpdateActionError(result) !== null; | 125 | return getDesktopUpdateActionError(result) !== null; |
| 126 | } | 126 | } |
| 127 | 127 | ||
| 128 | export function shouldHighlightDesktopUpdateError(state: DesktopUpdateState | null): boolean { | ||
| 129 | if (!state || state.status !== "error") return false; | ||
| 130 | return state.errorContext === "download" || state.errorContext === "install"; | ||
| 131 | } | ||
| 132 | |||
| 133 | export function canCheckForUpdate(state: DesktopUpdateState | null): boolean { | 128 | export function canCheckForUpdate(state: DesktopUpdateState | null): boolean { |
| 134 | if (!state || !state.enabled) return false; | 129 | if (!state || !state.enabled) return false; |
| 135 | return ( | 130 | return ( |
apps/web/src/composer-logic.ts-2| ... | @@ -221,8 +221,6 @@ export function isCollapsedCursorAdjacentToInlineToken( | ... | @@ -221,8 +221,6 @@ export function isCollapsedCursorAdjacentToInlineToken( |
| 221 | return false; | 221 | return false; |
| 222 | } | 222 | } |
| 223 | 223 | ||
| 224 | export const isCollapsedCursorAdjacentToMention = isCollapsedCursorAdjacentToInlineToken; | ||
| 225 | |||
| 226 | export function detectComposerTrigger(text: string, cursorInput: number): ComposerTrigger | null { | 224 | export function detectComposerTrigger(text: string, cursorInput: number): ComposerTrigger | null { |
| 227 | const cursor = clampCursor(text, cursorInput); | 225 | const cursor = clampCursor(text, cursorInput); |
| 228 | const lineStart = text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; | 226 | const lineStart = text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; |
apps/web/src/composerDraftStore.ts-8| ... | @@ -4117,11 +4117,3 @@ export function finalizePromotedDraftThreadByRef(threadRef: ScopedThreadRef): vo | ... | @@ -4117,11 +4117,3 @@ export function finalizePromotedDraftThreadByRef(threadRef: ScopedThreadRef): vo |
| 4117 | } | 4117 | } |
| 4118 | clearBackgroundDraftSubmissionByRef(threadRef); | 4118 | clearBackgroundDraftSubmissionByRef(threadRef); |
| 4119 | } | 4119 | } |
| 4120 | |||
| 4121 | export function finalizePromotedDraftThreadsByRef( | ||
| 4122 | serverThreadRefs: Iterable<ScopedThreadRef>, | ||
| 4123 | ): void { | ||
| 4124 | for (const threadRef of serverThreadRefs) { | ||
| 4125 | finalizePromotedDraftThreadByRef(threadRef); | ||
| 4126 | } | ||
| 4127 | } |
apps/web/src/keybindings.ts-16| ... | @@ -411,14 +411,6 @@ export function isDiffToggleShortcut( | ... | @@ -411,14 +411,6 @@ export function isDiffToggleShortcut( |
| 411 | return matchesCommandShortcut(event, keybindings, "diff.toggle", options); | 411 | return matchesCommandShortcut(event, keybindings, "diff.toggle", options); |
| 412 | } | 412 | } |
| 413 | 413 | ||
| 414 | export function isPreviewToggleShortcut( | ||
| 415 | event: ShortcutEventLike, | ||
| 416 | keybindings: ResolvedKeybindingsConfig, | ||
| 417 | options?: ShortcutMatchOptions, | ||
| 418 | ): boolean { | ||
| 419 | return matchesCommandShortcut(event, keybindings, "preview.toggle", options); | ||
| 420 | } | ||
| 421 | |||
| 422 | export function isPreviewRefreshShortcut( | 414 | export function isPreviewRefreshShortcut( |
| 423 | event: ShortcutEventLike, | 415 | event: ShortcutEventLike, |
| 424 | keybindings: ResolvedKeybindingsConfig, | 416 | keybindings: ResolvedKeybindingsConfig, |
| ... | @@ -427,14 +419,6 @@ export function isPreviewRefreshShortcut( | ... | @@ -427,14 +419,6 @@ export function isPreviewRefreshShortcut( |
| 427 | return matchesCommandShortcut(event, keybindings, "preview.refresh", options); | 419 | return matchesCommandShortcut(event, keybindings, "preview.refresh", options); |
| 428 | } | 420 | } |
| 429 | 421 | ||
| 430 | export function isPreviewFocusUrlShortcut( | ||
| 431 | event: ShortcutEventLike, | ||
| 432 | keybindings: ResolvedKeybindingsConfig, | ||
| 433 | options?: ShortcutMatchOptions, | ||
| 434 | ): boolean { | ||
| 435 | return matchesCommandShortcut(event, keybindings, "preview.focusUrl", options); | ||
| 436 | } | ||
| 437 | |||
| 438 | export function isChatNewShortcut( | 422 | export function isChatNewShortcut( |
| 439 | event: ShortcutEventLike, | 423 | event: ShortcutEventLike, |
| 440 | keybindings: ResolvedKeybindingsConfig, | 424 | keybindings: ResolvedKeybindingsConfig, |
apps/web/src/lib/contextWindow.ts-22| ... | @@ -25,28 +25,6 @@ export type ContextWindowSnapshot = NullableContextWindowUsage & { | ... | @@ -25,28 +25,6 @@ export type ContextWindowSnapshot = NullableContextWindowUsage & { |
| 25 | readonly updatedAt: string; | 25 | readonly updatedAt: string; |
| 26 | }; | 26 | }; |
| 27 | 27 | ||
| 28 | /** Map a provider driver kind to a user-facing display name. */ | ||
| 29 | export function formatProviderDisplayName(provider: string | null | undefined): string { | ||
| 30 | if (!provider) return "This agent"; | ||
| 31 | switch (provider) { | ||
| 32 | case "claudeAgent": | ||
| 33 | case "claude": | ||
| 34 | return "Claude"; | ||
| 35 | case "codex": | ||
| 36 | return "Codex"; | ||
| 37 | case "cursor": | ||
| 38 | return "Cursor"; | ||
| 39 | case "opencode": | ||
| 40 | return "OpenCode"; | ||
| 41 | default: { | ||
| 42 | // Title-case unknown driver kinds so they read reasonably. | ||
| 43 | const trimmed = provider.replace(/Agent$/i, "").trim(); | ||
| 44 | if (trimmed.length === 0) return provider; | ||
| 45 | return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | export function deriveLatestContextWindowSnapshot( | 28 | export function deriveLatestContextWindowSnapshot( |
| 51 | activities: ReadonlyArray<OrchestrationThreadActivity>, | 29 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 52 | ): ContextWindowSnapshot | null { | 30 | ): ContextWindowSnapshot | null { |
apps/web/src/lib/utils.ts+1-7| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { CommandId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts"; | 1 | import { MessageId, ProjectId, ThreadId } from "@t3tools/contracts"; |
| 2 | import { type CxOptions, cx } from "class-variance-authority"; | 2 | import { type CxOptions, cx } from "class-variance-authority"; |
| 3 | import * as Encoding from "effect/Encoding"; | 3 | import * as Encoding from "effect/Encoding"; |
| 4 | import { twMerge } from "tailwind-merge"; | 4 | import { twMerge } from "tailwind-merge"; |
| ... | @@ -16,10 +16,6 @@ export function isWindowsPlatform(platform: string): boolean { | ... | @@ -16,10 +16,6 @@ export function isWindowsPlatform(platform: string): boolean { |
| 16 | return /^win(dows)?/i.test(platform); | 16 | return /^win(dows)?/i.test(platform); |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | export function isLinuxPlatform(platform: string): boolean { | ||
| 20 | return /linux/i.test(platform); | ||
| 21 | } | ||
| 22 | |||
| 23 | export function normalizeSearchText(value: string): string { | 19 | export function normalizeSearchText(value: string): string { |
| 24 | return value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, " ").trim(); | 20 | return value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, " ").trim(); |
| 25 | } | 21 | } |
| ... | @@ -46,8 +42,6 @@ export function randomUUID(): string { | ... | @@ -46,8 +42,6 @@ export function randomUUID(): string { |
| 46 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; | 42 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; |
| 47 | } | 43 | } |
| 48 | 44 | ||
| 49 | export const newCommandId = (): CommandId => CommandId.make(randomUUID()); | ||
| 50 | |||
| 51 | export const newProjectId = (): ProjectId => ProjectId.make(randomUUID()); | 45 | export const newProjectId = (): ProjectId => ProjectId.make(randomUUID()); |
| 52 | 46 | ||
| 53 | export const newThreadId = (): ThreadId => ThreadId.make(randomUUID()); | 47 | export const newThreadId = (): ThreadId => ThreadId.make(randomUUID()); |
apps/web/src/portDiscoveryState.ts-19| ... | @@ -85,22 +85,3 @@ export function useThreadDiscoveredPorts(input: { | ... | @@ -85,22 +85,3 @@ export function useThreadDiscoveredPorts(input: { |
| 85 | [input.threadId, ports], | 85 | [input.threadId, ports], |
| 86 | ); | 86 | ); |
| 87 | } | 87 | } |
| 88 | |||
| 89 | export function useTerminalDiscoveredPorts(input: { | ||
| 90 | readonly environmentId: EnvironmentId | null; | ||
| 91 | readonly threadId: ThreadId | null; | ||
| 92 | readonly terminalId: string | null; | ||
| 93 | }): ReadonlyArray<DiscoveredLocalServer> { | ||
| 94 | const ports = useDiscoveredPorts(input.environmentId); | ||
| 95 | return useMemo( | ||
| 96 | () => | ||
| 97 | input.threadId && input.terminalId | ||
| 98 | ? ports.filter( | ||
| 99 | (port) => | ||
| 100 | port.terminal?.threadId === input.threadId && | ||
| 101 | port.terminal.terminalId === input.terminalId, | ||
| 102 | ) | ||
| 103 | : EMPTY_PORTS, | ||
| 104 | [input.terminalId, input.threadId, ports], | ||
| 105 | ); | ||
| 106 | } |
apps/web/src/providerInstances.ts-11| ... | @@ -299,17 +299,6 @@ export function getProviderInstanceEntry( | ... | @@ -299,17 +299,6 @@ export function getProviderInstanceEntry( |
| 299 | return deriveProviderInstanceEntries(providers).find((entry) => entry.instanceId === instanceId); | 299 | return deriveProviderInstanceEntries(providers).find((entry) => entry.instanceId === instanceId); |
| 300 | } | 300 | } |
| 301 | 301 | ||
| 302 | /** | ||
| 303 | * Model list for a specific instance. Returns `[]` when the instance isn't | ||
| 304 | * present so callers don't have to thread optionality through render code. | ||
| 305 | */ | ||
| 306 | export function getProviderInstanceModels( | ||
| 307 | providers: ReadonlyArray<ServerProvider>, | ||
| 308 | instanceId: ProviderInstanceId, | ||
| 309 | ): ReadonlyArray<ServerProviderModel> { | ||
| 310 | return getProviderInstanceEntry(providers, instanceId)?.models ?? []; | ||
| 311 | } | ||
| 312 | |||
| 313 | /** | 302 | /** |
| 314 | * Default model slug for a specific instance: its declared built-in default, | 303 | * Default model slug for a specific instance: its declared built-in default, |
| 315 | * then its first built-in model, then any model it reports, then the driver-level default. Custom | 304 | * then its first built-in model, then any model it reports, then the driver-level default. Custom |
apps/web/src/providerModels.ts-18| ... | @@ -38,14 +38,6 @@ export function getProviderSnapshot( | ... | @@ -38,14 +38,6 @@ export function getProviderSnapshot( |
| 38 | return providers.find((candidate) => candidate.instanceId === defaultInstanceId); | 38 | return providers.find((candidate) => candidate.instanceId === defaultInstanceId); |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | export function getProviderDisplayName( | ||
| 42 | providers: ReadonlyArray<ServerProvider>, | ||
| 43 | provider: ProviderDriverKind, | ||
| 44 | ): string { | ||
| 45 | const snapshot = getProviderSnapshot(providers, provider); | ||
| 46 | return snapshot?.displayName?.trim() || formatProviderDriverKindLabel(provider); | ||
| 47 | } | ||
| 48 | |||
| 49 | export function getProviderInteractionModeToggle( | 41 | export function getProviderInteractionModeToggle( |
| 50 | providers: ReadonlyArray<ServerProvider>, | 42 | providers: ReadonlyArray<ServerProvider>, |
| 51 | provider: ProviderDriverKind, | 43 | provider: ProviderDriverKind, |
| ... | @@ -53,16 +45,6 @@ export function getProviderInteractionModeToggle( | ... | @@ -53,16 +45,6 @@ export function getProviderInteractionModeToggle( |
| 53 | return getProviderSnapshot(providers, provider)?.showInteractionModeToggle ?? true; | 45 | return getProviderSnapshot(providers, provider)?.showInteractionModeToggle ?? true; |
| 54 | } | 46 | } |
| 55 | 47 | ||
| 56 | export function isProviderEnabled( | ||
| 57 | providers: ReadonlyArray<ServerProvider>, | ||
| 58 | provider: ProviderDriverKind, | ||
| 59 | ): boolean { | ||
| 60 | if (providers.length === 0) { | ||
| 61 | return true; | ||
| 62 | } | ||
| 63 | return getProviderSnapshot(providers, provider)?.enabled ?? false; | ||
| 64 | } | ||
| 65 | |||
| 66 | // Resolve an instance selection to the correlated live driver. If the | 48 | // Resolve an instance selection to the correlated live driver. If the |
| 67 | // instance is absent, fall back to a live enabled provider instead of | 49 | // instance is absent, fall back to a live enabled provider instead of |
| 68 | // inferring a driver from the missing instance id. | 50 | // inferring a driver from the missing instance id. |
apps/web/src/routes/-chatIndexTitlebar.test.ts deleted-29| ... | @@ -1,29 +0,0 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | // Regression coverage compares the onboarding header with the shared titlebar contract. | ||
| 3 | import * as NodeFS from "node:fs"; | ||
| 4 | |||
| 5 | import { describe, expect, it } from "vite-plus/test"; | ||
| 6 | |||
| 7 | describe("hosted static onboarding header", () => { | ||
| 8 | it("uses the shared workspace topbar geometry", () => { | ||
| 9 | const routeSource = NodeFS.readFileSync(new URL("./_chat.index.tsx", import.meta.url), "utf8"); | ||
| 10 | const onboardingStart = routeSource.indexOf("function HostedStaticOnboardingState()"); | ||
| 11 | const onboardingEnd = routeSource.indexOf('<Empty className="flex-1">', onboardingStart); | ||
| 12 | |||
| 13 | expect(onboardingStart).toBeGreaterThanOrEqual(0); | ||
| 14 | expect(onboardingEnd).toBeGreaterThan(onboardingStart); | ||
| 15 | |||
| 16 | const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd); | ||
| 17 | |||
| 18 | expect(onboardingHeader).toContain("<WorkspacePageHeader"); | ||
| 19 | expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/); | ||
| 20 | |||
| 21 | const headerSource = NodeFS.readFileSync( | ||
| 22 | new URL("../components/WorkspacePageHeader.tsx", import.meta.url), | ||
| 23 | "utf8", | ||
| 24 | ); | ||
| 25 | expect(headerSource).toContain("h-[var(--workspace-topbar-height)]"); | ||
| 26 | expect(headerSource).toContain("min-h-[var(--workspace-topbar-height)]"); | ||
| 27 | expect(headerSource).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS"); | ||
| 28 | }); | ||
| 29 | }); | ||
apps/web/src/themePalette.ts-2| ... | @@ -1521,8 +1521,6 @@ export class ThemeLibraryStorageError extends Schema.TaggedErrorClass<ThemeLibra | ... | @@ -1521,8 +1521,6 @@ export class ThemeLibraryStorageError extends Schema.TaggedErrorClass<ThemeLibra |
| 1521 | } | 1521 | } |
| 1522 | } | 1522 | } |
| 1523 | 1523 | ||
| 1524 | export const isThemeLibraryStorageError = Schema.is(ThemeLibraryStorageError); | ||
| 1525 | |||
| 1526 | function saveCustomThemes( | 1524 | function saveCustomThemes( |
| 1527 | storedThemes: ReadonlyArray<unknown>, | 1525 | storedThemes: ReadonlyArray<unknown>, |
| 1528 | themes: ReadonlyArray<ThemeDefinition>, | 1526 | themes: ReadonlyArray<ThemeDefinition>, |
pnpm-lock.yaml-6| ... | @@ -232,9 +232,6 @@ importers: | ... | @@ -232,9 +232,6 @@ importers: |
| 232 | '@noble/curves': | 232 | '@noble/curves': |
| 233 | specifier: 'catalog:' | 233 | specifier: 'catalog:' |
| 234 | version: 1.9.1 | 234 | version: 1.9.1 |
| 235 | '@noble/hashes': | ||
| 236 | specifier: 'catalog:' | ||
| 237 | version: 1.8.0 | ||
| 238 | '@pierre/diffs': | 235 | '@pierre/diffs': |
| 239 | specifier: 'catalog:' | 236 | specifier: 'catalog:' |
| 240 | version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) | 237 | version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) |
| ... | @@ -479,9 +476,6 @@ importers: | ... | @@ -479,9 +476,6 @@ importers: |
| 479 | '@effect/platform-node': | 476 | '@effect/platform-node': |
| 480 | specifier: 4.0.0-beta.103 | 477 | specifier: 4.0.0-beta.103 |
| 481 | version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(ioredis@5.11.0)(utf-8-validate@6.0.6) | 478 | version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(ioredis@5.11.0)(utf-8-validate@6.0.6) |
| 482 | '@effect/platform-node-shared': | ||
| 483 | specifier: 4.0.0-beta.103 | ||
| 484 | version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(utf-8-validate@6.0.6) | ||
| 485 | '@effect/sql-sqlite-bun': | 479 | '@effect/sql-sqlite-bun': |
| 486 | specifier: 4.0.0-beta.103 | 480 | specifier: 4.0.0-beta.103 |
| 487 | version: 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6)) | 481 | version: 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6)) |
vite.config.ts-1| ... | @@ -37,7 +37,6 @@ export default defineConfig({ | ... | @@ -37,7 +37,6 @@ export default defineConfig({ |
| 37 | "**/routeTree.gen.ts", | 37 | "**/routeTree.gen.ts", |
| 38 | "apps/mobile/android/**", | 38 | "apps/mobile/android/**", |
| 39 | "apps/mobile/ios/**", | 39 | "apps/mobile/ios/**", |
| 40 | "apps/web/public/mockServiceWorker.js", | ||
| 41 | "apps/web/src/lib/vendor/qrcodegen.ts", | 40 | "apps/web/src/lib/vendor/qrcodegen.ts", |
| 42 | "apps/mobile/uniwind-types.d.ts", | 41 | "apps/mobile/uniwind-types.d.ts", |
| 43 | "*.icon/**", | 42 | "*.icon/**", |