From df13391fb1b321b810cfab0e721ca858c65b931f Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Fri, 6 Nov 2026 19:42:48 +0000 Subject: [PATCH 01/67] fix(azure-devops): a fork's same-named branch no longer hides the branch's own pull request (#29817) Co-authored-by: Claude Opus 5.5 --- .../AzureDevOpsSourceControlProvider.test.ts | 18 ++++++++++++++++++ .../server/AzureDevOpsSourceControlProvider.ts | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts index 70f0ce906fe4f..5b4e8e91c436e 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts @@ -1,8 +1,11 @@ import { processOutput } from "@t3tools/source-control-testing/TestSourceControlHost"; import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { ChildProcessSpawner } from "effect/process"; +import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; const { parseAuth } = AzureDevOpsSourceControlProvider.discovery; @@ -25,3 +28,18 @@ it("reads the signed-in Azure account and says when az is signed out", () => { ); assert.strictEqual(parseAuth(processOutput("\n")).status, "unknown"); }); + +it.effect( + "looks up a branch's pull requests a full page at a time, past forks' same-named branches", + () => + Effect.gen(function* () { + const provider = yield* AzureDevOpsSourceControlProvider.make; + assert.deepStrictEqual( + provider.headBranchProbe?.({ + headSelectors: ["octocat:feature", "feature"], + state: "open", + }), + { headSelectors: ["feature"], limit: 100 }, + ); + }).pipe(Effect.provide(Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}))), +); diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts index 64c1ada9bc137..c63fa639f2dee 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts @@ -76,6 +76,11 @@ export const make = Effect.gen(function* () { return SourceControlProvider.SourceControlProvider.of({ kind: SourceControlProviderKind.make("azure-devops"), + // Azure lists fork pull requests by source branch with no owner filter, so read a full page. + headBranchProbe: ({ headSelectors }) => ({ + headSelectors: headSelectors.filter((selector) => !selector.includes(":")), + limit: 100, + }), listChangeRequests: Effect.fn("AzureDevOpsSourceControlProvider.listChangeRequests")( function (input) { const source = SourceControlProvider.sourceControlRefFromInput(input); From 8a942c2121c7a2d5b8a164c1ffc247334c62137b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 11:54:54 -0800 Subject: [PATCH 02/67] perf(server): provider-thread reads start from owned rows, not every node of the thread (#31012) Co-authored-by: Claude Opus 5.5 --- .../orchestration-v2/ProjectionStore.test.ts | 56 +++++++++++++++++++ .../src/orchestration-v2/ProjectionStore.ts | 22 +++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts index 9ae1bc4882d82..8ff160f0cc2ac 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts @@ -472,6 +472,62 @@ it.effect("record reads restrict provider histories to delivery identities", () })).providerThreads, [], ); + const ownerNodeId = NodeId.make("node:delivery-identities:owner"); + yield* store.apply({ + id: EventId.make("event:delivery-identities:owner-node"), + type: "node.updated", + threadId, + runId: RunId.make("run:delivery-identities:rolled-back"), + nodeId: ownerNodeId, + driver, + occurredAt: now, + payload: { + id: ownerNodeId, + threadId, + runId: RunId.make("run:delivery-identities:rolled-back"), + rootNodeId: NodeId.make("node:delivery-identities:rolled-back"), + parentNodeId: NodeId.make("node:delivery-identities:rolled-back"), + kind: "subagent", + status: "completed", + countsForRun: false, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: now, + }, + }); + yield* store.apply({ + id: EventId.make("event:delivery-identities:owned"), + type: "provider-thread.updated", + threadId, + occurredAt: now, + payload: { + id: ProviderThreadId.make("provider-thread:owned"), + appThreadId: ThreadId.make("thread:delivery-identities:child"), + driver, + providerInstanceId, + providerSessionId: null, + ownerNodeId, + nativeThreadRef: null, + nativeConversationHeadRef: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + status: "idle", + firstRunOrdinal: 2, + createdAt: now, + updatedAt: now, + }, + }); + assert.deepEqual( + (yield* store.getThreadRecords(threadId, ["providerThreads"])).providerThreads.map( + (row) => row.id, + ), + ["provider-thread:history", "provider-thread:selected", "provider-thread:owned"], + ); // Unselected history must not be decoded, even if its payload is unreadable. yield* sql`UPDATE orchestration_v2_projection_provider_threads SET payload_json = '{}' WHERE provider_thread_id = 'provider-thread:history'`; diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts index 2f8c972e7ded9..dea68ee7723d1 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -1979,6 +1979,16 @@ export const layer: Layer.Layer = OR item.payload_json LIKE '%"viewedImagePath"%' OR item.payload_json LIKE '%"toolName":"%delegate_task"%')`; + // Start from the few owned provider threads; a thread can have a large settled node history. + const nodesOwningProviderThreads = (threadId: ThreadId) => sql` + SELECT owner.node_id + FROM orchestration_v2_projection_provider_threads AS owned + INDEXED BY orchestration_v2_projection_provider_threads_owner_idx + CROSS JOIN orchestration_v2_projection_nodes AS owner + ON owner.node_id = owned.owner_node_id + WHERE owned.owner_node_id IS NOT NULL AND owner.thread_id = ${threadId} + `; + const keepRecordedRunField = (payload: Statement.Fragment, path: string) => sql` CASE WHEN json_type(excluded.payload_json, ${path}) IS NULL @@ -3500,11 +3510,7 @@ export const layer: Layer.Layer = SELECT payload_json FROM orchestration_v2_projection_provider_threads WHERE (thread_id = ${threadId} - OR owner_node_id IN ( - SELECT node_id - FROM orchestration_v2_projection_nodes - WHERE thread_id = ${threadId} - ) + OR owner_node_id IN (${nodesOwningProviderThreads(threadId)}) OR provider_thread_id IN ( SELECT provider_thread_id FROM orchestration_v2_projection_subagents @@ -5085,7 +5091,7 @@ export const layer: Layer.Layer = sql`SELECT payload_json FROM orchestration_v2_projection_provider_threads WHERE provider_thread_id = ${target.providerThreadId} AND ( thread_id = ${threadId} - OR owner_node_id IN (SELECT node_id FROM orchestration_v2_projection_nodes WHERE thread_id = ${threadId}) + OR owner_node_id IN (${nodesOwningProviderThreads(threadId)}) OR provider_thread_id IN (SELECT provider_thread_id FROM orchestration_v2_projection_subagents WHERE thread_id = ${threadId}) )`, sql`SELECT payload_json FROM orchestration_v2_projection_provider_turns @@ -5325,7 +5331,7 @@ export const layer: Layer.Layer = sql`SELECT payload_json FROM orchestration_v2_projection_provider_threads WHERE provider_thread_id = ${run?.providerThreadId ?? null} AND (thread_id = ${threadId} - OR owner_node_id IN (SELECT node_id FROM orchestration_v2_projection_nodes WHERE thread_id = ${threadId}) + OR owner_node_id IN (${nodesOwningProviderThreads(threadId)}) OR provider_thread_id IN (SELECT provider_thread_id FROM orchestration_v2_projection_subagents WHERE thread_id = ${threadId}))`, sql<{ readonly ordinal: number }>`SELECT ordinal_within_scope AS ordinal FROM orchestration_v2_projection_checkpoints @@ -5364,7 +5370,7 @@ export const layer: Layer.Layer = sql`SELECT payload_json FROM orchestration_v2_projection_provider_threads WHERE provider_thread_id = ${target.providerThreadId} AND (thread_id = ${threadId} - OR owner_node_id IN (SELECT node_id FROM orchestration_v2_projection_nodes WHERE thread_id = ${threadId}) + OR owner_node_id IN (${nodesOwningProviderThreads(threadId)}) OR provider_thread_id IN (SELECT provider_thread_id FROM orchestration_v2_projection_subagents WHERE thread_id = ${threadId}))`, sql`SELECT payload_json FROM orchestration_v2_projection_checkpoints WHERE thread_id = ${threadId} AND checkpoint_id = ${target.checkpointId}`, From 50f0dacbd7695309a58d69318fab2fff502a91ea Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 11:55:02 -0800 Subject: [PATCH 03/67] perf(server): stop tracing the per-event background-work probe (#31584) Co-authored-by: Claude Opus 5.5 --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 5 ++--- apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 10edb77c5011a..df80f1316fb77 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -11809,9 +11809,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( } return false; }), - hasPendingBackgroundWorkForThread: Effect.fn( - "ClaudeAdapterV2.hasPendingBackgroundWorkForThread", - )(function* (providerThread, options) { + // Untraced: the run's event stream calls this once per provider event. + hasPendingBackgroundWorkForThread: Effect.fnUntraced(function* (providerThread, options) { const nativeThreadId = providerThread.nativeThreadRef?.nativeId; if (nativeThreadId === undefined || nativeThreadId === null) { return false; diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 07c4e4e72eca7..369608a289d2e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -7071,9 +7071,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( } return false; }), - hasPendingBackgroundWorkForThread: Effect.fn( - "CodexAdapterV2.hasPendingBackgroundWorkForThread", - )(function* (providerThread) { + // Untraced: the run's event stream calls this once per provider event. + hasPendingBackgroundWorkForThread: Effect.fnUntraced(function* (providerThread) { const contexts = [ ...(yield* Ref.get(activeTurns)).values(), ...(yield* Ref.get(settledTurns)).values(), From 8ee2f35eeb2d2a4b9603b5a1e0b7328289c97ac8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 11:55:10 -0800 Subject: [PATCH 04/67] perf(server): stop tracing each transcript file in usage scans (#31586) Co-authored-by: Claude Opus 5.5 --- apps/server/src/usage/UsageService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 7e2e8586ed95f..8876f0af131f1 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -439,9 +439,10 @@ export const make = Effect.gen(function* () { /** * Reads run concurrently, and the caller stores updates in walk order rather * than completion order: saved records of deleted transcripts aggregate in - * cache order, where the first copy of a duplicate wins. + * cache order, where the first copy of a duplicate wins. Untraced: a scan + * reads thousands of files, so scanTranscriptDir records the counts instead. */ - const readFileRecords = Effect.fn("UsageService.readFileRecords")(function* ( + const readFileRecords = Effect.fnUntraced(function* ( filePath: string, size: number, mtimeMs: number, @@ -597,6 +598,10 @@ export const make = Effect.gen(function* () { return { path, records, malformedRecords }; }); const unread = failedPaths + read.filter((file) => file.failed).length; + yield* Effect.annotateCurrentSpan({ + "usage.files": files.length, + "usage.files_parsed": read.filter((file) => file.update !== undefined).length, + }); return { provider, dir, From c76f94e16a5a6ba22d9f60680efe9b78fd6c64b7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 11:55:17 -0800 Subject: [PATCH 05/67] perf(cursor): stop tracing the per-event account usage hash (#31017) Co-authored-by: Claude Opus 5.5 --- packages/provider-cursor/src/server/accountUsage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/provider-cursor/src/server/accountUsage.ts b/packages/provider-cursor/src/server/accountUsage.ts index 2719960eed033..c26d691ad16ee 100644 --- a/packages/provider-cursor/src/server/accountUsage.ts +++ b/packages/provider-cursor/src/server/accountUsage.ts @@ -58,7 +58,8 @@ const billedEventIdentity = (...fields: readonly unknown[]) => JSON.stringify(fi const textEncoder = new TextEncoder(); -const accountHash = Effect.fn("accountHash")(function* (value: string) { +// Untraced: each usage scan hashes every billed event. +const accountHash = Effect.fnUntraced(function* (value: string) { const digest = yield* Digest.Digest; return Hex.encode(yield* digest.digest(Digest.sha256, textEncoder.encode(value))); }); From 9db5d56366441ac5795587b0ea3cdbca327c3143 Mon Sep 17 00:00:00 2001 From: Sudharsan <126713117+Sidreyas@users.noreply.github.com> Date: Sat, 7 Nov 2026 01:29:17 +0530 Subject: [PATCH 06/67] fix(muse): recover from unavailable session views (#32154) --- .../provider-muse/src/server/adapter.test.ts | 36 +++++++++++++++---- packages/provider-muse/src/server/adapter.ts | 4 +++ packages/provider-muse/src/server/protocol.ts | 1 + 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/provider-muse/src/server/adapter.test.ts b/packages/provider-muse/src/server/adapter.test.ts index 847c2b4551c59..cc84894ac6ef9 100644 --- a/packages/provider-muse/src/server/adapter.test.ts +++ b/packages/provider-muse/src/server/adapter.test.ts @@ -850,16 +850,33 @@ describe("MuseAdapterV2", () => { }).pipe(Effect.provide(testLayer)), ); - it.effect( - "closes a newly opened host when native session registration returns another identity", - () => + it.effect.each([ + { + reason: "another identity", + method: "session/start", + sessionId: "unexpected-session", + initialNativeThreadId: undefined, + history: { items: [] }, + }, + { + reason: "an unavailable projection", + method: "session/resume", + sessionId: "saved-session", + initialNativeThreadId: "saved-session", + history: { items: null, mode: "none", noneReason: "projectionUnavailable" }, + }, + ])( + "closes a newly opened host when native session registration returns $reason", + ({ method, sessionId, initialNativeThreadId, history }) => Effect.gen(function* () { const fake = yield* makeFakeMuse(); - fake.queueResponse("session/start", { - session: { sessionId: "unexpected-session", modelId: MODEL, activeTurnId: null }, - history: { items: [] }, + fake.queueResponse(method, { + session: { sessionId, modelId: MODEL, activeTurnId: null }, + history, }); - const result = yield* makeHarness(fake).pipe(Effect.exit); + const result = yield* makeHarness(fake, INSTANCE_ID, initialNativeThreadId).pipe( + Effect.exit, + ); assert.strictEqual(result._tag, "Failure"); yield* Effect.promise(() => fake.host.exited); assert.strictEqual(fake.closeCount(), 1); @@ -1441,6 +1458,11 @@ describe("MuseAdapterV2", () => { text: "Previous answer", }; fake.setHistory([nativeItem]); + fake.queueResponse("session/resume", { + session: { sessionId: "saved-session", modelId: MODEL, activeTurnId: null }, + viewCursor: "", + history: { items: null, mode: "none", noneReason: "excluded" }, + }); const harness = yield* makeHarness(fake, INSTANCE_ID, "saved-session"); const resumed = yield* fake.takeCall("session/resume"); assert.strictEqual(resumed.params.sessionId, "saved-session"); diff --git a/packages/provider-muse/src/server/adapter.ts b/packages/provider-muse/src/server/adapter.ts index b29f8d51947bf..fd1526a428c6e 100644 --- a/packages/provider-muse/src/server/adapter.ts +++ b/packages/provider-muse/src/server/adapter.ts @@ -1879,6 +1879,10 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( ); if (result.session.sessionId !== nativeSessionId) return yield* protocolError("Muse returned a different session identity"); + if (result.history?.noneReason === "projectionUnavailable") + return yield* protocolError( + "Muse cannot stream this session because its live view is unavailable", + ); if (requestedId) yield* request("session/setApprovalMode", { mode: museApprovalMode(args.runtimePolicy.runtimeMode), diff --git a/packages/provider-muse/src/server/protocol.ts b/packages/provider-muse/src/server/protocol.ts index 9c3e7c40d957b..0c1fdad7db3b2 100644 --- a/packages/provider-muse/src/server/protocol.ts +++ b/packages/provider-muse/src/server/protocol.ts @@ -70,6 +70,7 @@ export const MuseSessionResult = Schema.Struct({ history: Schema.optional( Schema.Struct({ items: Schema.NullOr(Schema.Array(MuseItem)), + noneReason: Schema.optional(Schema.String), snapshot: Schema.optional( Schema.NullOr( Schema.Struct({ From c09060b62bfe3e065e2ef4c273dfe55b0f10d0b3 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Sat, 7 Nov 2026 01:30:15 +0530 Subject: [PATCH 07/67] feat(web): sort and filter transcription models (#32045) --- .../settings/VoiceSettingsPanel.tsx | 100 ++++++++++++++---- .../settings/speechModelBrowser.test.ts | 100 ++++++++++++++++++ .../components/settings/speechModelBrowser.ts | 45 ++++++++ 3 files changed, 224 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/components/settings/speechModelBrowser.test.ts create mode 100644 apps/web/src/components/settings/speechModelBrowser.ts diff --git a/apps/web/src/components/settings/VoiceSettingsPanel.tsx b/apps/web/src/components/settings/VoiceSettingsPanel.tsx index c08844bafe09b..b0fd5d2fff5d5 100644 --- a/apps/web/src/components/settings/VoiceSettingsPanel.tsx +++ b/apps/web/src/components/settings/VoiceSettingsPanel.tsx @@ -31,6 +31,7 @@ import { MicIcon, RefreshCwIcon, StarIcon, + SlidersHorizontalIcon, Trash2Icon, XIcon, } from "lucide-react"; @@ -65,6 +66,19 @@ import { Badge } from "../ui/badge"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; +import { + Menu, + MenuCheckboxItem, + MenuGroup, + MenuGroupLabel, + MenuPopup, + MenuRadioGroup, + MenuRadioItem, + MenuRadioItemIndicator, + MenuSeparator, + MenuTrigger, +} from "../ui/menu"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { Textarea } from "../ui/textarea"; @@ -72,6 +86,7 @@ import { toastManager } from "../ui/toast"; import { MicrophoneTest } from "./MicrophoneTest"; import { LanguageRow, ModelBenchmarks, orderSpeechLanguages } from "./SpeechModelComparison"; import { languageLabel, SpeechLanguageSelect } from "./SpeechLanguageSelect"; +import { filterSpeechModels, type SpeechModelSort } from "./speechModelBrowser"; import { AdvancedSettings } from "./AdvancedSettings"; import { SETTINGS_PICKER_TRIGGER_CLASSNAME, @@ -110,12 +125,6 @@ const spellingsOf = (words: SpeechCustomWords) => [term, ...aliases].map((spelling) => spelling.toLocaleLowerCase()), ), ); -const modelSortOrder = (model: EnvironmentSpeechModel) => { - if (model.active) return 0; - if (model.state === "installed") return 1; - if (model.state === "downloading" || model.state === "verifying") return 2; - return model.recommended ? 3 : 4; -}; type ModelCardProps = { readonly model: EnvironmentSpeechModel; @@ -717,6 +726,8 @@ function SpeechModelBrowser(props: { catalog: { status, models, run, controlsDisabled, canCancel, requestedDownload }, } = props; const [search, setSearch] = useState(""); + const [sort, setSort] = useState("recommended"); + const [downloadedOnly, setDownloadedOnly] = useState(false); const [languageSearch, setLanguageSearch] = useState(""); const [selection, setSelection] = useState(null); const preferred = navigator.languages.map((language) => language.toLowerCase().split("-")[0]!); @@ -729,14 +740,20 @@ function SpeechModelBrowser(props: { (code) => !needle || code.includes(needle) || languageLabel(code).toLocaleLowerCase().includes(needle), ); - const query = search.trim().toLocaleLowerCase(); - const sortedModels = models - .filter( - (model) => - selectedLanguages.every((language) => model.languages.includes(language)) && - (!query || (model.name + " " + model.description).toLocaleLowerCase().includes(query)), - ) - .toSorted((left, right) => modelSortOrder(left) - modelSortOrder(right)); + const accuracyLanguage = selectedLanguages[0] ?? "en"; + const sortLabels: Record = { + recommended: "Recommended", + size: "Smallest download", + accuracy: `Best ${languageLabel(accuracyLanguage)} accuracy`, + speed: "Fastest transcription", + }; + const sortedModels = filterSpeechModels(models, { + search, + languages: selectedLanguages, + downloadedOnly, + sort, + accuracyLanguage, + }); const activeModel = models.find((model) => model.active); const renderedModels = activeModel && !sortedModels.includes(activeModel) @@ -808,13 +825,54 @@ function SpeechModelBrowser(props: { ? "Models for " + selectedLanguages.map(languageLabel).join(", ") : "All models"}

- setSearch(e.target.value)} - /> + + setSearch(e.target.value)} + /> + + + } + aria-label={`Sort and filter models: ${sortLabels[sort]}${downloadedOnly ? ", downloaded only" : ""}`} + > + + + {sortLabels[sort]} + + {downloadedOnly && 1} + + + + Sort by + setSort(value as SpeechModelSort)} + > + {(Object.keys(sortLabels) as SpeechModelSort[]).map((value) => ( + + + {sortLabels[value]} + + + + ))} + + + + + Downloaded only + + + + +
{ + expect( + filterSpeechModels(models, { + search: "", + languages: [], + downloadedOnly: false, + sort, + accuracyLanguage, + }).map((entry) => entry.id), + ).toEqual(expected); + expect(models[0]?.id).toBe("active"); + }, +); + +it("combines text, all selected languages, and completed downloads without retaining the active model", () => { + const options = { + search: " LOCAL ", + languages: ["en", "fr"], + downloadedOnly: true, + sort: "size", + accuracyLanguage: "en", + } as const; + expect(filterSpeechModels(models, options).map((entry) => entry.id)).toEqual(["fast", "active"]); + expect( + filterSpeechModels(models, { ...options, search: "fast" }).map((entry) => entry.id), + ).toEqual(["fast"]); + expect(filterSpeechModels(models, { ...options, search: "small" })).toEqual([]); +}); diff --git a/apps/web/src/components/settings/speechModelBrowser.ts b/apps/web/src/components/settings/speechModelBrowser.ts new file mode 100644 index 0000000000000..d9cab8d1ef0f0 --- /dev/null +++ b/apps/web/src/components/settings/speechModelBrowser.ts @@ -0,0 +1,45 @@ +import type { EnvironmentSpeechModel } from "@t3tools/contracts"; + +export type SpeechModelSort = "recommended" | "size" | "accuracy" | "speed"; + +const modelSortOrder = (model: EnvironmentSpeechModel) => { + if (model.active) return 0; + if (model.state === "installed") return 1; + if (model.state === "downloading" || model.state === "verifying") return 2; + return model.recommended ? 3 : 4; +}; + +export function filterSpeechModels( + models: readonly EnvironmentSpeechModel[], + options: { + readonly search: string; + readonly languages: readonly string[]; + readonly downloadedOnly: boolean; + readonly sort: SpeechModelSort; + readonly accuracyLanguage: string; + }, +) { + const query = options.search.trim().toLocaleLowerCase(); + const score = (model: EnvironmentSpeechModel) => { + switch (options.sort) { + case "size": + return model.size; + case "accuracy": + return model.benchmark?.accuracy[options.accuracyLanguage]?.errorPercent ?? Infinity; + case "speed": + return model.benchmark?.secondsPer30s ?? Infinity; + case "recommended": + return modelSortOrder(model); + } + }; + return models + .filter( + (model) => + options.languages.every((language) => model.languages.includes(language)) && + (!options.downloadedOnly || model.state === "installed") && + (!query || (model.name + " " + model.description).toLocaleLowerCase().includes(query)), + ) + .toSorted( + (left, right) => score(left) - score(right) || modelSortOrder(left) - modelSortOrder(right), + ); +} From 6fe0fdf088019052e5b4430882c7d968d23976d7 Mon Sep 17 00:00:00 2001 From: Adam Otradovec <37932502+aotradovec@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:00:26 +0100 Subject: [PATCH 08/67] fix(azure-devops): existing pull requests are found for branches containing # (#31928) Co-authored-by: Adam Otradovec Co-authored-by: Yash Singh --- .../src/server/AzureDevOpsCli.test.ts | 32 +++++++++++++++++++ .../src/server/AzureDevOpsCli.ts | 8 ++++- .../server/AzureDevOpsPullRequestCli.test.ts | 16 ++++++++++ .../src/server/AzureDevOpsPullRequestCli.ts | 3 +- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts index 3cb69661997b6..9b40406093155 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts @@ -233,6 +233,38 @@ describe("AzureDevOpsCli.layer", () => { }).pipe(Effect.provide(layer)), ); + it.effect("percent-encodes the source branch while preserving recognized ref prefixes", () => + Effect.gen(function* () { + mockRun.mockReturnValueOnce(Effect.succeed(processOutput("[]"))); + + const az = yield* AzureDevOpsCli.AzureDevOpsCli; + yield* az.listPullRequests({ + cwd: "/somewhere", + headSelector: "user/feat/1-(#2)-slug", + state: "open", + }); + + const args = mockRun.mock.calls.at(-1)?.[0].args ?? []; + assert.strictEqual(args[args.indexOf("--source-branch") + 1], "user%2Ffeat%2F1-(%232)-slug"); + + for (const prefix of ["refs/heads/", "refs/pull/", "refs/tags/"]) { + mockRun.mockReturnValueOnce(Effect.succeed(processOutput("[]"))); + yield* az.listPullRequests({ + cwd: "/somewhere", + headSelector: "fallback", + source: { refName: `${prefix}user/feat/1-(#2)-slug` }, + state: "open", + }); + + const refArgs = mockRun.mock.calls.at(-1)?.[0].args ?? []; + assert.strictEqual( + refArgs[refArgs.indexOf("--source-branch") + 1], + `${prefix}user%2Ffeat%2F1-(%232)-slug`, + ); + } + }).pipe(Effect.provide(layer)), + ); + it.effect("preserves VCS causes without copying upstream details into messages", () => Effect.gen(function* () { const cause = new VcsProcessExitError({ diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts index 57b9f4086cdaa..eb6151d336263 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts @@ -401,6 +401,11 @@ export const make = Effect.gen(function* () { return AzureDevOpsCli.of({ execute, listPullRequests: Effect.fn("AzureDevOpsCli.listPullRequests")(function (input) { + const branch = SourceControlProvider.sourceBranch(input); + // az checks literal ref prefixes before constructing the request URL. + const refPrefix = + ["refs/heads/", "refs/pull/", "refs/tags/"].find((prefix) => branch.startsWith(prefix)) ?? + ""; return executeJson({ cwd: input.cwd, args: [ @@ -410,7 +415,8 @@ export const make = Effect.gen(function* () { "--detect", "true", "--source-branch", - SourceControlProvider.sourceBranch(input), + // az puts this in the query string unescaped; a `#` would cut the request. + refPrefix + encodeURIComponent(branch.slice(refPrefix.length)), "--status", toAzureStatus(input.state), "--top", diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts index 4b2e46b5a1e93..9bbe1468f8f54 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts @@ -850,6 +850,22 @@ layer("AzureDevOpsPullRequestCli.layer", (it) => { }), ); + it.effect("escapes characters in a file path that would cut the query string", () => + Effect.gen(function* () { + mockedExecute.mockReturnValueOnce(Effect.succeed(output(json({ content: "x" })))); + const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; + + yield* cli.readItemContent({ + cwd: "/w", + location: { project: "platform", repository: "web" }, + path: "docs/C#/a&b.md", + commit: "a".repeat(40), + }); + + expect(mockedExecute.mock.calls.at(-1)?.[0].args).toContain("path=%2Fdocs%2FC%23%2Fa%26b.md"); + }), + ); + it.effect("reports a pull request it cannot place as its own outcome", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce( diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts index 07d9c2a2057e7..81c9b2e68fb6e 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts @@ -603,7 +603,8 @@ export const make = Effect.gen(function* () { resource: "items", routeParameters: repositoryRoute(input.location), queryParameters: [ - `path=${toItemPath(input.path)}`, + // `az devops invoke` does not escape query values, so `#` or `&` in a path would truncate it. + `path=${encodeURIComponent(toItemPath(input.path))}`, "versionDescriptor.versionType=commit", `versionDescriptor.version=${input.commit}`, "includeContent=true", From bdb8d5dfc067134f460ea7757e62355fe5f9b2a7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 12:11:54 -0800 Subject: [PATCH 09/67] perf(server): background PR discovery no longer delays git status after a restart (#32162) Co-authored-by: Claude Opus 5.5 --- .../ThreadPullRequestService.ts | 2 + .../ThreadSettlementService.ts | 2 + apps/server/src/vcs/GitVcsDriver.ts | 5 ++ apps/server/src/vcs/GitVcsDriverCore.test.ts | 50 +++++++++++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 8 ++- 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/ThreadPullRequestService.ts b/apps/server/src/orchestration-v2/ThreadPullRequestService.ts index 1cb01acd4ed9c..d84c532d06ecd 100644 --- a/apps/server/src/orchestration-v2/ThreadPullRequestService.ts +++ b/apps/server/src/orchestration-v2/ThreadPullRequestService.ts @@ -20,6 +20,7 @@ import * as Stream from "effect/Stream"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as GitManager from "../git/GitManager.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as ProjectStore from "./ProjectStore.ts"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; @@ -347,6 +348,7 @@ export const make = Effect.gen(function* () { const worker = yield* makeDrainableWorker((request: RefreshRequest) => synchronize(request).pipe( + Effect.provideService(GitVcsDriver.BackgroundGitWork, true), Effect.catchCauseIf( (cause) => !Cause.hasInterruptsOnly(cause), (cause) => diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.ts index aedbb73fc5501..8c852426da323 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.ts @@ -31,6 +31,7 @@ import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as ServerSettings from "../serverSettings.ts"; import { forkParked } from "../serverActivation.ts"; import * as TerminalManager from "../terminal/Manager.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as ProjectStore from "./ProjectStore.ts"; import { sourceControlDefinitions } from "../sourceControl/sourceControlDefinitions.ts"; import * as Orchestrator from "./Orchestrator.ts"; @@ -556,6 +557,7 @@ export const make = Effect.gen(function* () { threadId?: ThreadId, ) => sweep(mergedPullRequest, threadId).pipe( + Effect.provideService(GitVcsDriver.BackgroundGitWork, true), Effect.catchCauseIf( (cause) => !Cause.hasInterruptsOnly(cause), (cause) => diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index d120605d6961f..0d97d43c3e9d6 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -301,6 +301,11 @@ export interface GitRemoteStatusOptions { readonly refreshUpstream?: boolean; } +/** Set by background discovery so its Git commands run in their own small lane, not the user's. */ +export const BackgroundGitWork = Context.Reference("t3/vcs/BackgroundGitWork", { + defaultValue: () => false, +}); + export class GitVcsDriver extends Context.Service< GitVcsDriver, { diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 39427404093af..728c20c034ee2 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -14,6 +14,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; +import * as Queue from "effect/Queue"; import * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; @@ -129,6 +130,55 @@ const makeDriverReleasingLock = ( return { driver, failures }; }); +it.effect("runs background Git in its own lane so user commands never queue behind it", () => + Effect.gen(function* () { + const gate = yield* Deferred.make(); + const starts = yield* Queue.unbounded(); + const spawner = ChildProcessSpawner.make((command) => + Effect.gen(function* () { + yield* Queue.offer( + starts, + ChildProcess.isStandardCommand(command) ? String(command.args[0]) : "", + ); + return ChildProcessSpawner.makeHandle({ + ...makeSuccessfulHandle("ok"), + exitCode: Deferred.await(gate).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))), + }); + }), + ); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provide(layerServerConfig), + ); + const run = (args: ReadonlyArray) => + driver.execute({ operation: "test.gitLane", cwd: "/repo", args: [...args] }); + const background = yield* Effect.forEach( + Array.from({ length: 12 }), + () => run(["background"]), + { + concurrency: "unbounded", + }, + ).pipe(Effect.provideService(GitVcsDriver.BackgroundGitWork, true), Effect.forkChild); + const foreground = yield* Effect.forEach(Array.from({ length: 4 }), () => run(["foreground"]), { + concurrency: "unbounded", + }).pipe(Effect.forkChild); + + yield* TestClock.adjust("2 seconds"); + const started = yield* Queue.takeAll(starts); + assert.deepStrictEqual([...started].toSorted(), [ + "background", + "background", + "foreground", + "foreground", + "foreground", + "foreground", + ]); + yield* Deferred.succeed(gate, undefined); + assert.equal((yield* Fiber.join(background)).length, 12); + assert.equal((yield* Fiber.join(foreground)).length, 4); + }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))), +); + describe("Git output collection", () => { // Git writes in arbitrary chunk sizes; one byte per chunk splits every // multibyte character across reads. diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index d1199f39c6542..85a7e524b1a63 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -63,6 +63,7 @@ const branchBaseConfigKey = (branch: string) => `branch.${branch}.t3-merge-base` */ const legacyBranchBaseConfigKey = (branch: string) => `branch.${branch}.gh-merge-base`; const gitProcesses = Semaphore.makeUnsafe(8); +const backgroundGitProcesses = Semaphore.makeUnsafe(2); const WORKTREE_REMOVE_TIMEOUT_MS = Duration.toMillis(Duration.minutes(5)); const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]"; @@ -1123,7 +1124,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* (execution) => input.timeoutMs === null || (input.timeoutMs ?? DEFAULT_TIMEOUT_MS) > DEFAULT_TIMEOUT_MS ? execution - : gitProcesses.withPermits(1)(execution), + : Effect.gen(function* () { + const lane = (yield* GitVcsDriver.BackgroundGitWork) + ? backgroundGitProcesses + : gitProcesses; + return yield* lane.withPermits(1)(execution); + }), Effect.withSpan(input.operation, { kind: "client", attributes: { From 8bf1b64cae37abbff80a5cf98d2ae0f41dba5264 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 12:27:56 -0800 Subject: [PATCH 10/67] fix(web): the citation comment box opens at the quote instead of sliding in from the corner (#32141) Co-authored-by: Claude Opus 5.5 --- .../AssistantCitationChip.browser.test.tsx | 20 +++++++++++++++++++ apps/web/src/components/ui/popover.tsx | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/AssistantCitationChip.browser.test.tsx b/apps/web/src/components/chat/AssistantCitationChip.browser.test.tsx index deee8d7afa69d..e1b66e1fed8f2 100644 --- a/apps/web/src/components/chat/AssistantCitationChip.browser.test.tsx +++ b/apps/web/src/components/chat/AssistantCitationChip.browser.test.tsx @@ -129,6 +129,26 @@ describe("citation comment source disappearance", () => { }); describe("citation comment popup position", () => { + it("opens at its anchor instead of sliding in from the window corner", async () => { + using source = setup(); + await using _screen = await mount( + source.sourceAnchor, + vi.fn(() => true), + ); + const positioner = () => + document.querySelector('[data-slot="popover-positioner"]'); + await vi.waitFor(() => expect(positioner()?.style.opacity).not.toBe("0")); + + const moves = positioner()! + .getAnimations() + .filter( + (animation) => + animation instanceof CSSTransition && + ["transform", "top", "left"].includes(animation.transitionProperty), + ); + expect(moves).toEqual([]); + }); + it("stays on screen when the cited text ends in a code block's hidden overflow", async () => { using _source = setup(); const message = document.createElement("div"); diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx index 788c8a1043def..e03aee4011593 100644 --- a/apps/web/src/components/ui/popover.tsx +++ b/apps/web/src/components/ui/popover.tsx @@ -78,7 +78,9 @@ function PopoverPopup({ collisionAvoidance={collisionAvoidance} collisionPadding={variant === "panel" ? 0 : undefined} className={cn( - "h-(--positioner-height) max-h-(--available-height) w-(--positioner-width) max-w-(--available-width) transition-transform data-instant:transition-none", + "h-(--positioner-height) max-h-(--available-height) w-(--positioner-width) max-w-(--available-width)", + // Kept-mounted positioners first paint at the window origin, so a transform transition slides them in from there. + !keepMounted && "transition-transform data-instant:transition-none", variant === "panel" ? "z-(--z-sheet) w-[min(calc(var(--thread-details-panel-width)+--spacing(4)),calc(var(--anchor-width)---spacing(2)))] transition-[top] duration-150 ease-out data-instant:transition-[top] motion-reduce:transition-none motion-reduce:data-instant:transition-none" : "z-[130]", From 40dc2910ea87a4959325e9210c84dacef37e9c3c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 12:27:56 -0800 Subject: [PATCH 11/67] fix(clients): dictating a citation comment or preview annotation uses the full width (#32142) Co-authored-by: Claude Opus 5.5 --- .../src/preview/AnnotationStyles.generated.ts | 2 +- apps/desktop/src/preview/PickPreload.ts | 17 +++++++++++++++-- .../chat/AssistantCitationCommentEditor.tsx | 10 +++++++++- apps/web/src/components/chat/VoiceInputPill.tsx | 5 ++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/preview/AnnotationStyles.generated.ts b/apps/desktop/src/preview/AnnotationStyles.generated.ts index a8c70b5cecec5..3b8e2b7000896 100644 --- a/apps/desktop/src/preview/AnnotationStyles.generated.ts +++ b/apps/desktop/src/preview/AnnotationStyles.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/build-preview-annotation-css.mjs. Do not edit. export const previewAnnotationStyles = - '/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties;\n:root, :host {\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --blur-xl: 24px;\n --default-font-family: var(--t3-font-sans);\n --default-mono-font-family: var(--t3-font-mono);\n}\n*, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n}\nhtml, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', \'Noto Sans\', Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Noto Color Emoji\');\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n}\nhr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n}\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nh1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n}\na {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n}\nb, strong {\n font-weight: bolder;\n}\ncode, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \'Liberation Mono\', \'Courier New\', monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n}\nsmall {\n font-size: 80%;\n}\nsub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\ntable {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n}\n:-moz-focusring:where(:not(iframe)) {\n outline: auto;\n}\nprogress {\n vertical-align: baseline;\n}\nsummary {\n display: list-item;\n}\nol, ul, menu {\n list-style: none;\n}\nimg, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n}\nimg, video {\n max-width: 100%;\n height: auto;\n}\nbutton, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n}\n:where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n}\n:where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n}\n::file-selector-button {\n margin-inline-end: 4px;\n}\n::placeholder {\n opacity: 1;\n}\n@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n}\ntextarea {\n resize: vertical;\n}\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n}\n::-webkit-datetime-edit {\n display: inline-flex;\n}\n::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n}\n::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n}\n::-webkit-calendar-picker-indicator {\n line-height: 1;\n}\n:-moz-ui-invalid {\n box-shadow: none;\n}\nbutton, input:where([type=\'button\'], [type=\'reset\'], [type=\'submit\']), ::file-selector-button {\n appearance: button;\n}\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n}\n[hidden]:where(:not([hidden=\'until-found\'])) {\n display: none !important;\n}\n.pointer-events-auto {\n pointer-events: auto;\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.visible {\n visibility: visible;\n}\n.absolute {\n position: absolute;\n}\n.fixed {\n position: fixed;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-1\\/2 {\n top: calc(1 / 2 * 100%);\n}\n.top-2\\.5 {\n top: calc(var(--spacing) * 2.5);\n}\n.right-2 {\n right: calc(var(--spacing) * 2);\n}\n.left-1\\/2 {\n left: calc(1 / 2 * 100%);\n}\n.z-1 {\n z-index: 1;\n}\n.block {\n display: block;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.hidden {\n display: none;\n}\n.inline-flex {\n display: inline-flex;\n}\n.h-5 {\n height: calc(var(--spacing) * 5);\n}\n.h-7 {\n height: calc(var(--spacing) * 7);\n}\n.h-8 {\n height: calc(var(--spacing) * 8);\n}\n.h-9 {\n height: calc(var(--spacing) * 9);\n}\n.h-full {\n height: 100%;\n}\n.max-h-24 {\n max-height: calc(var(--spacing) * 24);\n}\n.max-h-\\[calc\\(100vh-16px\\)\\] {\n max-height: calc(100vh - 16px);\n}\n.max-h-\\[min\\(176px\\,calc\\(100vh-180px\\)\\)\\] {\n max-height: min(176px, calc(100vh - 180px));\n}\n.min-h-7 {\n min-height: calc(var(--spacing) * 7);\n}\n.min-h-8 {\n min-height: calc(var(--spacing) * 8);\n}\n.w-0\\.5 {\n width: calc(var(--spacing) * 0.5);\n}\n.w-6 {\n width: calc(var(--spacing) * 6);\n}\n.w-7 {\n width: calc(var(--spacing) * 7);\n}\n.w-8 {\n width: calc(var(--spacing) * 8);\n}\n.w-64 {\n width: calc(var(--spacing) * 64);\n}\n.w-\\[min\\(360px\\,calc\\(100vw-16px\\)\\)\\] {\n width: min(360px, calc(100vw - 16px));\n}\n.w-full {\n width: 100%;\n}\n.max-w-70 {\n max-width: calc(var(--spacing) * 70);\n}\n.min-w-0 {\n min-width: 0px;\n}\n.flex-1 {\n flex: 1;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.origin-center {\n transform-origin: center;\n}\n.-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.cursor-grab {\n cursor: grab;\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.resize {\n resize: both;\n}\n.resize-none {\n resize: none;\n}\n.appearance-none {\n appearance: none;\n}\n.grid-cols-\\[22px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 22px minmax(0,1fr);\n}\n.grid-cols-\\[82px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 82px minmax(0,1fr);\n}\n.flex-col {\n flex-direction: column;\n}\n.items-center {\n align-items: center;\n}\n.items-start {\n align-items: flex-start;\n}\n.justify-between {\n justify-content: space-between;\n}\n.justify-center {\n justify-content: center;\n}\n.justify-end {\n justify-content: flex-end;\n}\n.gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n}\n.gap-1 {\n gap: var(--spacing);\n}\n.gap-2 {\n gap: calc(var(--spacing) * 2);\n}\n.gap-px {\n gap: 1px;\n}\n.truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-y-hidden {\n overflow-y: hidden;\n}\n.rounded-full {\n border-radius: calc(infinity * 1px);\n}\n.rounded-lg {\n border-radius: var(--t3-radius);\n}\n.rounded-md {\n border-radius: calc(var(--t3-radius) - 2px);\n}\n.rounded-xl {\n border-radius: calc(var(--t3-radius) + 4px);\n}\n.border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n}\n.border-0 {\n border-style: var(--tw-border-style);\n border-width: 0px;\n}\n.border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n}\n.border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n}\n.border-border {\n border-color: var(--t3-border);\n}\n.border-input {\n border-color: var(--t3-input);\n}\n.border-primary {\n border-color: var(--t3-primary);\n}\n.border-transparent {\n border-color: transparent;\n}\n.border-b-transparent {\n border-bottom-color: transparent;\n}\n.bg-background {\n background-color: var(--t3-background);\n}\n.bg-muted {\n background-color: var(--t3-muted);\n}\n.bg-muted\\/40 {\n background-color: var(--t3-muted);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-muted) 40%, transparent);\n }\n}\n.bg-popover\\/95 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 95%, transparent);\n }\n}\n.bg-popover\\/96 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 96%, transparent);\n }\n}\n.bg-primary {\n background-color: var(--t3-primary);\n}\n.bg-primary\\/10 {\n background-color: var(--t3-primary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-primary) 10%, transparent);\n }\n}\n.bg-transparent {\n background-color: transparent;\n}\n.p-0 {\n padding: 0px;\n}\n.p-1 {\n padding: var(--spacing);\n}\n.p-2 {\n padding: calc(var(--spacing) * 2);\n}\n.px-0 {\n padding-inline: 0px;\n}\n.px-1 {\n padding-inline: var(--spacing);\n}\n.px-2 {\n padding-inline: calc(var(--spacing) * 2);\n}\n.px-2\\.5 {\n padding-inline: calc(var(--spacing) * 2.5);\n}\n.px-3 {\n padding-inline: calc(var(--spacing) * 3);\n}\n.py-1 {\n padding-block: var(--spacing);\n}\n.py-1\\.5 {\n padding-block: calc(var(--spacing) * 1.5);\n}\n.py-2 {\n padding-block: calc(var(--spacing) * 2);\n}\n.pb-2 {\n padding-bottom: calc(var(--spacing) * 2);\n}\n.text-right {\n text-align: right;\n}\n.font-mono {\n font-family: var(--t3-font-mono);\n}\n.font-sans {\n font-family: var(--t3-font-sans);\n}\n.text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n}\n.text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n}\n.text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n}\n.leading-5 {\n --tw-leading: calc(var(--spacing) * 5);\n line-height: calc(var(--spacing) * 5);\n}\n.font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n}\n.font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n}\n.font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n}\n.text-foreground {\n color: var(--t3-foreground);\n}\n.text-muted-foreground {\n color: var(--t3-muted-foreground);\n}\n.text-popover-foreground {\n color: var(--t3-popover-foreground);\n}\n.text-primary {\n color: var(--t3-primary);\n}\n.text-primary-foreground {\n color: var(--t3-primary-foreground);\n}\n.tabular-nums {\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-sm {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-xs {\n --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.ring-0 {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n}\n.backdrop-blur-xl {\n --tw-backdrop-blur: blur(var(--blur-xl));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n}\n.outline-none {\n --tw-outline-style: none;\n outline-style: none;\n}\n.select-none {\n -webkit-user-select: none;\n user-select: none;\n}\n.placeholder\\:text-muted-foreground::placeholder {\n color: var(--t3-muted-foreground);\n}\n@media (hover: hover) {\n .hover\\:bg-accent:hover {\n background-color: var(--t3-accent);\n }\n .hover\\:bg-primary\\/90:hover {\n background-color: var(--t3-primary);\n }\n @supports (color: color-mix(in lab, red, red)) {\n .hover\\:bg-primary\\/90:hover {\n background-color: color-mix(in oklab, var(--t3-primary) 90%, transparent);\n }\n }\n .hover\\:text-accent-foreground:hover {\n color: var(--t3-accent-foreground);\n }\n}\n.focus\\:border-b-primary:focus {\n border-bottom-color: var(--t3-primary);\n}\n.focus\\:ring-0:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.focus\\:outline-none:focus {\n --tw-outline-style: none;\n outline-style: none;\n}\n.disabled\\:pointer-events-none:disabled {\n pointer-events: none;\n}\n.disabled\\:opacity-60:disabled {\n opacity: 60%;\n}\n:host {\n --t3-font-sans: "DM Sans Variable", "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,\n sans-serif;\n --t3-font-mono: "SF Mono", "SFMono-Regular", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;\n --t3-radius: 0.625rem;\n --t3-background: white;\n --t3-foreground: oklch(0.269 0 0);\n --t3-popover: white;\n --t3-popover-foreground: oklch(0.269 0 0);\n --t3-primary: oklch(0.488 0.217 264);\n --t3-primary-foreground: white;\n --t3-destructive: oklch(0.577 0.245 27.325);\n --t3-muted: rgb(0 0 0 / 4%);\n --t3-muted-foreground: oklch(0.556 0 0);\n --t3-accent: rgb(0 0 0 / 4%);\n --t3-accent-foreground: oklch(0.269 0 0);\n --t3-border: rgb(0 0 0 / 8%);\n --t3-input: rgb(0 0 0 / 10%);\n --t3-ring: oklch(0.488 0.217 264);\n color: var(--t3-foreground);\n font-family: var(--t3-font-sans);\n}\n* {\n box-sizing: border-box;\n border-color: var(--t3-border);\n}\nbutton, input, select, textarea {\n font: inherit;\n}\nbutton:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {\n outline: 2px solid var(--t3-ring);\n @supports (color: color-mix(in lab, red, red)) {\n outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);\n }\n outline-offset: -2px;\n}\n[hidden] {\n display: none !important;\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ordinal {\n syntax: "*";\n inherits: false;\n}\n@property --tw-slashed-zero {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-figure {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-spacing {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-fraction {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@layer properties {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-ordinal: initial;\n --tw-slashed-zero: initial;\n --tw-numeric-figure: initial;\n --tw-numeric-spacing: initial;\n --tw-numeric-fraction: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n }\n}\n'; + '/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties;\n:root, :host {\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --blur-xl: 24px;\n --default-font-family: var(--t3-font-sans);\n --default-mono-font-family: var(--t3-font-mono);\n}\n*, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n}\nhtml, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', \'Noto Sans\', Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Noto Color Emoji\');\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n}\nhr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n}\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nh1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n}\na {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n}\nb, strong {\n font-weight: bolder;\n}\ncode, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \'Liberation Mono\', \'Courier New\', monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n}\nsmall {\n font-size: 80%;\n}\nsub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\ntable {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n}\n:-moz-focusring:where(:not(iframe)) {\n outline: auto;\n}\nprogress {\n vertical-align: baseline;\n}\nsummary {\n display: list-item;\n}\nol, ul, menu {\n list-style: none;\n}\nimg, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n}\nimg, video {\n max-width: 100%;\n height: auto;\n}\nbutton, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n}\n:where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n}\n:where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n}\n::file-selector-button {\n margin-inline-end: 4px;\n}\n::placeholder {\n opacity: 1;\n}\n@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n}\ntextarea {\n resize: vertical;\n}\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n}\n::-webkit-datetime-edit {\n display: inline-flex;\n}\n::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n}\n::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n}\n::-webkit-calendar-picker-indicator {\n line-height: 1;\n}\n:-moz-ui-invalid {\n box-shadow: none;\n}\nbutton, input:where([type=\'button\'], [type=\'reset\'], [type=\'submit\']), ::file-selector-button {\n appearance: button;\n}\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n}\n[hidden]:where(:not([hidden=\'until-found\'])) {\n display: none !important;\n}\n.pointer-events-auto {\n pointer-events: auto;\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.visible {\n visibility: visible;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border-width: 0;\n}\n.absolute {\n position: absolute;\n}\n.fixed {\n position: fixed;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-1\\/2 {\n top: calc(1 / 2 * 100%);\n}\n.top-2\\.5 {\n top: calc(var(--spacing) * 2.5);\n}\n.right-2 {\n right: calc(var(--spacing) * 2);\n}\n.left-1\\/2 {\n left: calc(1 / 2 * 100%);\n}\n.z-1 {\n z-index: 1;\n}\n.block {\n display: block;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.hidden {\n display: none;\n}\n.inline-flex {\n display: inline-flex;\n}\n.h-5 {\n height: calc(var(--spacing) * 5);\n}\n.h-7 {\n height: calc(var(--spacing) * 7);\n}\n.h-8 {\n height: calc(var(--spacing) * 8);\n}\n.h-full {\n height: 100%;\n}\n.max-h-24 {\n max-height: calc(var(--spacing) * 24);\n}\n.max-h-\\[calc\\(100vh-16px\\)\\] {\n max-height: calc(100vh - 16px);\n}\n.max-h-\\[min\\(176px\\,calc\\(100vh-180px\\)\\)\\] {\n max-height: min(176px, calc(100vh - 180px));\n}\n.min-h-7 {\n min-height: calc(var(--spacing) * 7);\n}\n.min-h-8 {\n min-height: calc(var(--spacing) * 8);\n}\n.w-0\\.5 {\n width: calc(var(--spacing) * 0.5);\n}\n.w-6 {\n width: calc(var(--spacing) * 6);\n}\n.w-7 {\n width: calc(var(--spacing) * 7);\n}\n.w-8 {\n width: calc(var(--spacing) * 8);\n}\n.w-\\[min\\(360px\\,calc\\(100vw-16px\\)\\)\\] {\n width: min(360px, calc(100vw - 16px));\n}\n.w-full {\n width: 100%;\n}\n.max-w-70 {\n max-width: calc(var(--spacing) * 70);\n}\n.min-w-0 {\n min-width: 0px;\n}\n.flex-1 {\n flex: 1;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.origin-center {\n transform-origin: center;\n}\n.-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.cursor-grab {\n cursor: grab;\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.resize {\n resize: both;\n}\n.resize-none {\n resize: none;\n}\n.appearance-none {\n appearance: none;\n}\n.grid-cols-\\[22px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 22px minmax(0,1fr);\n}\n.grid-cols-\\[82px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 82px minmax(0,1fr);\n}\n.flex-col {\n flex-direction: column;\n}\n.items-center {\n align-items: center;\n}\n.items-start {\n align-items: flex-start;\n}\n.justify-between {\n justify-content: space-between;\n}\n.justify-center {\n justify-content: center;\n}\n.justify-end {\n justify-content: flex-end;\n}\n.gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n}\n.gap-1 {\n gap: var(--spacing);\n}\n.gap-2 {\n gap: calc(var(--spacing) * 2);\n}\n.gap-px {\n gap: 1px;\n}\n.truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-y-hidden {\n overflow-y: hidden;\n}\n.rounded-full {\n border-radius: calc(infinity * 1px);\n}\n.rounded-lg {\n border-radius: var(--t3-radius);\n}\n.rounded-md {\n border-radius: calc(var(--t3-radius) - 2px);\n}\n.rounded-xl {\n border-radius: calc(var(--t3-radius) + 4px);\n}\n.border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n}\n.border-0 {\n border-style: var(--tw-border-style);\n border-width: 0px;\n}\n.border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n}\n.border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n}\n.border-border {\n border-color: var(--t3-border);\n}\n.border-input {\n border-color: var(--t3-input);\n}\n.border-primary {\n border-color: var(--t3-primary);\n}\n.border-transparent {\n border-color: transparent;\n}\n.border-b-transparent {\n border-bottom-color: transparent;\n}\n.bg-background {\n background-color: var(--t3-background);\n}\n.bg-muted {\n background-color: var(--t3-muted);\n}\n.bg-muted\\/40 {\n background-color: var(--t3-muted);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-muted) 40%, transparent);\n }\n}\n.bg-popover\\/95 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 95%, transparent);\n }\n}\n.bg-popover\\/96 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 96%, transparent);\n }\n}\n.bg-primary {\n background-color: var(--t3-primary);\n}\n.bg-primary\\/10 {\n background-color: var(--t3-primary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-primary) 10%, transparent);\n }\n}\n.bg-transparent {\n background-color: transparent;\n}\n.p-0 {\n padding: 0px;\n}\n.p-1 {\n padding: var(--spacing);\n}\n.p-2 {\n padding: calc(var(--spacing) * 2);\n}\n.px-0 {\n padding-inline: 0px;\n}\n.px-1 {\n padding-inline: var(--spacing);\n}\n.px-2 {\n padding-inline: calc(var(--spacing) * 2);\n}\n.px-2\\.5 {\n padding-inline: calc(var(--spacing) * 2.5);\n}\n.px-3 {\n padding-inline: calc(var(--spacing) * 3);\n}\n.py-1 {\n padding-block: var(--spacing);\n}\n.py-1\\.5 {\n padding-block: calc(var(--spacing) * 1.5);\n}\n.py-2 {\n padding-block: calc(var(--spacing) * 2);\n}\n.pb-2 {\n padding-bottom: calc(var(--spacing) * 2);\n}\n.text-right {\n text-align: right;\n}\n.font-mono {\n font-family: var(--t3-font-mono);\n}\n.font-sans {\n font-family: var(--t3-font-sans);\n}\n.text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n}\n.text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n}\n.text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n}\n.leading-5 {\n --tw-leading: calc(var(--spacing) * 5);\n line-height: calc(var(--spacing) * 5);\n}\n.font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n}\n.font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n}\n.font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n}\n.text-foreground {\n color: var(--t3-foreground);\n}\n.text-muted-foreground {\n color: var(--t3-muted-foreground);\n}\n.text-popover-foreground {\n color: var(--t3-popover-foreground);\n}\n.text-primary {\n color: var(--t3-primary);\n}\n.text-primary-foreground {\n color: var(--t3-primary-foreground);\n}\n.tabular-nums {\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-sm {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-xs {\n --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.ring-0 {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n}\n.backdrop-blur-xl {\n --tw-backdrop-blur: blur(var(--blur-xl));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n}\n.outline-none {\n --tw-outline-style: none;\n outline-style: none;\n}\n.select-none {\n -webkit-user-select: none;\n user-select: none;\n}\n.placeholder\\:text-muted-foreground::placeholder {\n color: var(--t3-muted-foreground);\n}\n@media (hover: hover) {\n .hover\\:bg-accent:hover {\n background-color: var(--t3-accent);\n }\n .hover\\:bg-primary\\/90:hover {\n background-color: var(--t3-primary);\n }\n @supports (color: color-mix(in lab, red, red)) {\n .hover\\:bg-primary\\/90:hover {\n background-color: color-mix(in oklab, var(--t3-primary) 90%, transparent);\n }\n }\n .hover\\:text-accent-foreground:hover {\n color: var(--t3-accent-foreground);\n }\n}\n.focus\\:border-b-primary:focus {\n border-bottom-color: var(--t3-primary);\n}\n.focus\\:ring-0:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.focus\\:outline-none:focus {\n --tw-outline-style: none;\n outline-style: none;\n}\n.disabled\\:pointer-events-none:disabled {\n pointer-events: none;\n}\n.disabled\\:opacity-60:disabled {\n opacity: 60%;\n}\n:host {\n --t3-font-sans: "DM Sans Variable", "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,\n sans-serif;\n --t3-font-mono: "SF Mono", "SFMono-Regular", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;\n --t3-radius: 0.625rem;\n --t3-background: white;\n --t3-foreground: oklch(0.269 0 0);\n --t3-popover: white;\n --t3-popover-foreground: oklch(0.269 0 0);\n --t3-primary: oklch(0.488 0.217 264);\n --t3-primary-foreground: white;\n --t3-destructive: oklch(0.577 0.245 27.325);\n --t3-muted: rgb(0 0 0 / 4%);\n --t3-muted-foreground: oklch(0.556 0 0);\n --t3-accent: rgb(0 0 0 / 4%);\n --t3-accent-foreground: oklch(0.269 0 0);\n --t3-border: rgb(0 0 0 / 8%);\n --t3-input: rgb(0 0 0 / 10%);\n --t3-ring: oklch(0.488 0.217 264);\n color: var(--t3-foreground);\n font-family: var(--t3-font-sans);\n}\n* {\n box-sizing: border-box;\n border-color: var(--t3-border);\n}\nbutton, input, select, textarea {\n font: inherit;\n}\nbutton:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {\n outline: 2px solid var(--t3-ring);\n @supports (color: color-mix(in lab, red, red)) {\n outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);\n }\n outline-offset: -2px;\n}\n[hidden] {\n display: none !important;\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ordinal {\n syntax: "*";\n inherits: false;\n}\n@property --tw-slashed-zero {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-figure {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-spacing {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-fraction {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@layer properties {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-ordinal: initial;\n --tw-slashed-zero: initial;\n --tw-numeric-figure: initial;\n --tw-numeric-spacing: initial;\n --tw-numeric-fraction: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n }\n}\n'; diff --git a/apps/desktop/src/preview/PickPreload.ts b/apps/desktop/src/preview/PickPreload.ts index 0549e166dd634..f54963d10916f 100644 --- a/apps/desktop/src/preview/PickPreload.ts +++ b/apps/desktop/src/preview/PickPreload.ts @@ -633,6 +633,8 @@ function startAnnotation( " h-7 w-7 shrink-0 rounded-full border-primary bg-primary p-0 text-primary-foreground hover:bg-primary/90"; const voiceSkip = createButton("Skip", "Skip transcript cleanup"); voiceSkip.className += " h-7 shrink-0 rounded-full px-2.5"; + // createButton's rounded-md sorts after rounded-full in the generated CSS, so it would win. + for (const button of [voiceCancel, voiceFinish, voiceSkip]) button.classList.remove("rounded-md"); const voiceStatus = document.createElement("div"); voiceStatus.className = "flex h-7 min-w-0 flex-1 items-center gap-2"; voiceStatus.setAttribute("role", "status"); @@ -744,16 +746,27 @@ function startAnnotation( } const recording = state.phase === "recording"; const active = state.phase !== "idle" && state.phase !== "error"; + const wasActive = voicePill.parentElement === composerRow; voiceButton.hidden = !state.available || active; voiceButton.disabled = voiceBusy; const label = voiceSettings ? "Open voice settings" : "Start voice input"; voiceButton.title = !voiceSettings && state.shortcutLabel ? `${label} (${state.shortcutLabel})` : label; voiceButton.setAttribute("aria-label", label); - voiceRow.hidden = state.phase === "idle"; + // An active dictation takes the textarea's place; it takes focus once (sr-only) so Enter still attaches. + voiceRow.hidden = state.phase === "idle" || active; + comment.classList.toggle("sr-only", active); + if (active && !wasActive) comment.focus({ preventScroll: true }); + submit.hidden = active; + // Re-inserting on every level update would blur a focused Finish or Cancel. + if (active && voicePill.parentElement !== composerRow) comment.after(voicePill); + else if (!active && voicePill.parentElement !== voiceRow) voiceRow.appendChild(voicePill); voicePill.className = active - ? "flex h-9 w-64 min-w-0 items-center gap-2 rounded-full border border-border bg-background p-1" + ? "flex h-8 min-w-0 flex-1 items-center gap-2" : "flex min-w-0 items-center gap-1"; + // A draft that arrived while the textarea was sr-only was measured at 1px wide. + if (wasActive && !active) resizeComment(); + else if (phaseChanged) queueEditorLayout(); voiceCancel.title = state.phase === "error" ? "Dismiss voice input error" : "Cancel voice input"; voiceCancel.setAttribute("aria-label", voiceCancel.title); diff --git a/apps/web/src/components/chat/AssistantCitationCommentEditor.tsx b/apps/web/src/components/chat/AssistantCitationCommentEditor.tsx index cdd8e1249cfc7..f97c66c7f07e1 100644 --- a/apps/web/src/components/chat/AssistantCitationCommentEditor.tsx +++ b/apps/web/src/components/chat/AssistantCitationCommentEditor.tsx @@ -113,7 +113,15 @@ export function AssistantCitationCommentEditor({

) : null}
- +
+ +