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/mobile/src/features/pull-requests/PullRequestMarkdown.tsx b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx
index d829f6f649b86..41b93f966666b 100644
--- a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx
+++ b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx
@@ -1,4 +1,7 @@
-import { splitPullRequestBody } from "@t3tools/client-runtime/pull-request-markdown";
+import {
+ splitPullRequestBody,
+ type PullRequestBodySegment,
+} from "@t3tools/client-runtime/pull-request-markdown";
import { markdownTaskMarkerOffset } from "@t3tools/shared/markdownTaskList";
import type { ReactNode } from "react";
import { View } from "react-native";
@@ -116,49 +119,101 @@ export function PullRequestMarkdown(props: {
readonly checked: boolean;
}) => boolean;
}) {
- const { onTaskListChange } = props;
+ const mediaContext = useContext(PullRequestMediaContext);
+ const changeRequestUrl = useContext(ChangeRequestUrlContext);
const segments = useMemo(() => splitPullRequestBody(props.markdown), [props.markdown]);
const [undoneTicks, setUndoneTicks] = useState(0);
+ const gallery: FilePreviewItem[] = markdownImageSources(
+ props.imageGroupMarkdown ?? props.markdown,
+ )
+ .map((href) => (changeRequestUrl ? resolveChangeRequestMediaUrl(href, changeRequestUrl) : href))
+ .filter(
+ (uri) =>
+ /^https?:\/\//i.test(uri) &&
+ videoMimeType({ name: uri.split(/[?#]/)[0] ?? uri, mimeType: "" }) === null,
+ )
+ .map((uri) => pullRequestImageSource(uri, mediaContext));
+ const segmentProps = {
+ gallery,
+ lineBreaks: props.lineBreaks,
+ undoneTicks,
+ onTaskListChange: props.onTaskListChange,
+ onTaskListUndone: () => setUndoneTicks((count) => count + 1),
+ };
+ return (
+
+ {segments.map((segment) => (
+
+ ))}
+
+ );
+}
+
+function PullRequestBodySegmentView(props: {
+ readonly segment: PullRequestBodySegment;
+ readonly gallery: FilePreviewItem[];
+ readonly lineBreaks: boolean | undefined;
+ readonly undoneTicks: number;
+ readonly onTaskListChange:
+ | ((input: { readonly markerOffset: number; readonly checked: boolean }) => boolean)
+ | undefined;
+ readonly onTaskListUndone: () => void;
+}) {
+ const { segment, onTaskListChange } = props;
+ if (segment.kind === "markdown") {
+ return (
+ {
+ const offset = markdownTaskMarkerOffset(
+ segment.text,
+ event.index,
+ event.text,
+ event.checked,
+ );
+ const kept =
+ offset !== null &&
+ onTaskListChange({
+ markerOffset: segment.sourceOffset + offset,
+ checked: event.checked,
+ });
+ if (!kept) props.onTaskListUndone();
+ }
+ : undefined
+ }
+ />
+ );
+ }
+ if (segment.kind === "attachment") {
+ return ;
+ }
return (
- {segments.map((segment) =>
- segment.kind === "markdown" ? (
- {
- const offset = markdownTaskMarkerOffset(
- segment.text,
- event.index,
- event.text,
- event.checked,
- );
- const kept =
- offset !== null &&
- onTaskListChange({
- markerOffset: segment.sourceOffset + offset,
- checked: event.checked,
- });
- if (!kept) setUndoneTicks((count) => count + 1);
- }
- : undefined
- }
- />
- ) : (
-
- ),
- )}
+ {segment.rows.map((cells, rowIndex) => (
+ // oxlint-disable-next-line react/no-array-index-key -- rows have no identity besides position
+
+ {cells.map((cell, cellIndex) => (
+ // oxlint-disable-next-line react/no-array-index-key -- cells have no identity besides position
+
+ {cell.segments.map((cellSegment) => (
+
+ ))}
+
+ ))}
+
+ ))}
);
}
function PullRequestMarkdownContent(props: {
readonly markdown: string;
- readonly imageGroupMarkdown?: string;
+ readonly gallery: FilePreviewItem[];
readonly lineBreaks: boolean | undefined;
readonly onTaskListItemPress?: SelectableMarkdownTextProps["onTaskListItemPress"];
}) {
@@ -173,23 +228,13 @@ function PullRequestMarkdownContent(props: {
const openLink = (href: string) => {
void openPullRequest(href);
};
- const gallery: FilePreviewItem[] = markdownImageSources(
- props.imageGroupMarkdown ?? props.markdown,
- )
- .map(resolveMediaUrl)
- .filter(
- (uri) =>
- /^https?:\/\//i.test(uri) &&
- videoMimeType({ name: uri.split(/[?#]/)[0] ?? uri, mimeType: "" }) === null,
- )
- .map((uri) => pullRequestImageSource(uri, mediaContext));
const [preview, setPreview] = useState(null);
const renderImage: MarkdownImageRenderer = (image) => (
);
const styles = useMarkdownPreviewStyles();
return (
-
+
{
},
]);
});
+
+ it("collects videos from a before/after table", () => {
+ const media = splitPullRequestBody(
+ "\n| Before | After |
\n| \n\nhttps://drop.example.com/before.mp4\n\n | \n\nhttps://drop.example.com/after.mp4\n\n |
\n
",
+ );
+ expect(
+ pullRequestDescriptionContent({ type: "root" } as MarkdownNode, media).attachments,
+ ).toEqual([
+ { uri: "https://drop.example.com/before.mp4", caption: "Video", kind: "video" },
+ { uri: "https://drop.example.com/after.mp4", caption: "Video", kind: "video" },
+ ]);
+ });
});
diff --git a/apps/mobile/src/features/pull-requests/pullRequestDescriptionContent.ts b/apps/mobile/src/features/pull-requests/pullRequestDescriptionContent.ts
index 8d0d3c93edd43..242d1207fd0f1 100644
--- a/apps/mobile/src/features/pull-requests/pullRequestDescriptionContent.ts
+++ b/apps/mobile/src/features/pull-requests/pullRequestDescriptionContent.ts
@@ -37,7 +37,11 @@ export function pullRequestDescriptionContent(
if (node.type === "break") text.push(" ");
};
visit(document);
- for (const segment of media) {
+ const collect = (segment: PullRequestBodySegment) => {
+ if (segment.kind === "table") {
+ segment.rows.flat().forEach((cell) => cell.segments.forEach(collect));
+ return;
+ }
if (segment.kind === "attachment" && !attachments.has(segment.url)) {
const name = new URL(segment.url).pathname.split("/").pop();
attachments.set(segment.url, {
@@ -46,6 +50,7 @@ export function pullRequestDescriptionContent(
kind: segment.media,
});
}
- }
+ };
+ media.forEach(collect);
return { preview: text.join("").trim(), attachments: [...attachments.values()] };
}
diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
index 1173f5944e011..9f058c9d59d92 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
@@ -11820,9 +11820,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 2c0cfe050354b..13755cafe905a 100644
--- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
@@ -7077,9 +7077,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(),
diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts
index 1c27cbf12a186..e0ef02a995662 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 64d325c3f235f..4db0367fb35d5 100644
--- a/apps/server/src/orchestration-v2/ProjectionStore.ts
+++ b/apps/server/src/orchestration-v2/ProjectionStore.ts
@@ -1981,6 +1981,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
@@ -3502,11 +3512,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
@@ -5088,7 +5094,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
@@ -5328,7 +5334,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
@@ -5367,7 +5373,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}`,
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 83b7546579775..5a6a6fde1f769 100644
--- a/apps/server/src/orchestration-v2/ThreadSettlementService.ts
+++ b/apps/server/src/orchestration-v2/ThreadSettlementService.ts
@@ -34,6 +34,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";
@@ -578,6 +579,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/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,
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 3391af59a10b5..58e70368ce251 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -15,6 +15,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";
@@ -130,6 +131,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 0ceae16be8b0c..009d55d5c1612 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]";
@@ -1127,7 +1128,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: {
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/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}
-
+
+
+
{
+ 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),
+ );
+}
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]",
diff --git a/packages/client-runtime/src/pullRequestMarkdown.test.ts b/packages/client-runtime/src/pullRequestMarkdown.test.ts
index a5508e8197421..13327d8480a57 100644
--- a/packages/client-runtime/src/pullRequestMarkdown.test.ts
+++ b/packages/client-runtime/src/pullRequestMarkdown.test.ts
@@ -4,6 +4,7 @@ import {
changeRequestReferenceAutolinker,
resolveChangeRequestMediaUrl,
splitPullRequestBody,
+ type PullRequestBodySegment,
} from "./pullRequestMarkdown.ts";
describe("pull request body segmentation", () => {
@@ -26,17 +27,26 @@ describe("pull request body segmentation", () => {
]);
});
- it("places each prose run at its offset in the body", () => {
+ it("places each prose run at its offset in the body, including inside a media table", () => {
const body =
- '\n\n- [ ] Before\r\n\nhttps://example.com/shot.png\n\n\n
\n - [ ] code\n- [x] After\n';
- const prose = splitPullRequestBody(body).flatMap((segment) =>
- segment.kind === "markdown" ? [segment] : [],
- );
- expect(prose.map((segment) => segment.text)).toEqual([
+ '\n\n- [ ] Before\r\n\nhttps://example.com/shot.png\n\n\n
\n - [ ] code\n- [x] After\n\r\n
\r\n| \r\n\r\n- [ ] In a cell\r\n\r\n | \r\n\r\nhttps://example.com/b.mp4\r\n\r\n |
\r\n
\r\n- [ ] Last\n';
+ type Prose = Extract
;
+ const prose = (segments: ReadonlyArray): Prose[] =>
+ segments.flatMap((segment) =>
+ segment.kind === "markdown"
+ ? [segment]
+ : segment.kind === "table"
+ ? segment.rows.flat().flatMap((cell) => prose(cell.segments))
+ : [],
+ );
+ const runs = prose(splitPullRequestBody(body));
+ expect(runs.map((segment) => segment.text)).toEqual([
"- [ ] Before",
" - [ ] code\n- [x] After",
+ "- [ ] In a cell",
+ "- [ ] Last",
]);
- for (const segment of prose) {
+ for (const segment of runs) {
expect(body.slice(segment.sourceOffset, segment.sourceOffset + segment.text.length)).toBe(
segment.text,
);
@@ -166,6 +176,98 @@ describe("media on custom hosts", () => {
});
});
+describe("media tables", () => {
+ const before = "https://github.com/user-attachments/assets/c69c7c1c-ef2f-47dd-97e8-fe42e40712eb";
+ const after = "https://github.com/user-attachments/assets/1867b6ec-4f88-4248-b2df-6b0f9808105b";
+ const video = "https://example.com/a.mp4";
+
+ it("keeps GitHub's before/after video table together, splitting each cell on its own", () => {
+ const segments = splitPullRequestBody(
+ `Intro\n\n\n| Before | After |
\n\n| \n\n${before}\n\n | \n\n\n${after}\n\n | \n
\n
\n\nOutro`,
+ );
+ expect(segments.map((segment) => segment.kind)).toEqual(["markdown", "table", "markdown"]);
+ expect(segments[0]).toMatchObject({ text: "Intro" });
+ expect(segments[2]).toMatchObject({ text: "Outro" });
+ const table = segments[1]!;
+ if (table.kind !== "table") throw new Error("expected a table segment");
+ expect(
+ table.rows.map((row) =>
+ row.map((cell) =>
+ cell.segments.map((segment) =>
+ segment.kind === "attachment" ? `attachment:${segment.url}` : segment,
+ ),
+ ),
+ ),
+ ).toEqual([
+ [
+ [{ id: "markdown:0", kind: "markdown", text: "Before", sourceOffset: 23 }],
+ [{ id: "markdown:0", kind: "markdown", text: "After", sourceOffset: 38 }],
+ ],
+ [[`attachment:${before}`], [`attachment:${after}`]],
+ ]);
+ });
+
+ it("keeps a quoted `>` inside a cell's attributes out of its text", () => {
+ const [table] = splitPullRequestBody(
+ `\n| Caption | \n\n${video}\n\n |
\n
`,
+ );
+ expect(table).toMatchObject({
+ kind: "table",
+ rows: [[{ segments: [{ kind: "markdown", text: "Caption" }] }, {}]],
+ });
+ });
+
+ it("leaves a table without media links as markdown", () => {
+ const body = "";
+ expect(splitPullRequestBody(body)).toEqual([
+ { id: "markdown:0", kind: "markdown", text: body, sourceOffset: 0 },
+ ]);
+ });
+
+ it.each([
+ ["a caption", "\nBefore upgrading\n| ", " |
\n
"],
+ ["text outside cells", "\nBefore upgrading\n| ", " |
\n
"],
+ [
+ "text inside a row but outside cells",
+ "\nBefore upgrading| ",
+ " |
\n
",
+ ],
+ [
+ "a table nested in another",
+ "",
+ ],
+ ["an unclosed table", "\n", ""],
+ ["a spanning cell", '"],
+ [
+ "an indented cell",
+ "\n \n | \n Before\n | \n \n | ",
+ " | \n ",
+ ],
+ [
+ "a cell indented by a space and a tab",
+ "",
+ ],
+ ])("splits a media table with %s line by line, as before", (_name, open, close) => {
+ const body = `${open}\n\n${video}\n\n${close}`;
+ expect(splitPullRequestBody(body)).toEqual([
+ { id: "markdown:0", kind: "markdown", text: open, sourceOffset: 0 },
+ { id: "attachment:1", kind: "attachment", url: video, media: "video" },
+ ...(close
+ ? [
+ {
+ id: "markdown:2",
+ kind: "markdown",
+ text: close,
+ sourceOffset: body.length - close.length,
+ },
+ ]
+ : []),
+ ]);
+ });
+});
+
describe("change request reference autolinks", () => {
const urls = (provider: string, url: string, text: string) =>
changeRequestReferenceAutolinker({ provider, url })?.(text).map((link) => link.url) ?? null;
diff --git a/packages/client-runtime/src/pullRequestMarkdown.ts b/packages/client-runtime/src/pullRequestMarkdown.ts
index 92c7d6de70cf6..2ca59143ad296 100644
--- a/packages/client-runtime/src/pullRequestMarkdown.ts
+++ b/packages/client-runtime/src/pullRequestMarkdown.ts
@@ -14,8 +14,18 @@ export type PullRequestBodySegment =
readonly kind: "attachment";
readonly url: string;
readonly media: "video" | "image" | "unknown";
+ }
+ | {
+ readonly id: string;
+ readonly kind: "table";
+ readonly rows: ReadonlyArray>;
};
+type PullRequestBodyTableCell = {
+ readonly header: boolean;
+ readonly segments: ReadonlyArray;
+};
+
// `.` stops at the `\r` of a CRLF body, which is how GitHub stores one written in its web editor.
const FENCE_PATTERN = /^\s{0,3}((?:`{3,})|(?:~{3,}))(.*)\r?$/u;
/**
@@ -24,7 +34,8 @@ const FENCE_PATTERN = /^\s{0,3}((?:`{3,})|(?:~{3,}))(.*)\r?$/u;
*/
const VIDEO_TAG_MAX_LINES = 8;
-const INDENTED_CODE_PATTERN = /^(?: {4}|\t)/u;
+// A tab after up to three spaces still reaches the fourth column.
+const INDENTED_CODE_PATTERN = /^(?: {4}| {0,3}\t)/u;
const BARE_URL_PATTERN = /^(https?:\/\/\S+?)>?$/u;
const IMAGE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp|avif|heic|heif|svg)(?:$|[?#])/iu;
const MARKDOWN_LINK_PATTERN = /^\[[^\]]*\]\((https?:\/\/[^\s)]+)\)$/u;
@@ -35,6 +46,21 @@ const VIDEO_TAG_SRC_PATTERN = /<(?:video|source)\b[^>]*\bsrc\s*=\s*["']([^"']+)[
const STANDALONE_VIDEO_TAG_PATTERN = /^\s* |