| author | |
| committer | |
| log | c07575f573dd3a1af4f734297d17f7c951c95f10 |
| tree | 1041d550439dd87e9a1b38476a3e09e359f20d6b |
| parent | 66e39ca2aabde054bc50312a9c34f05dbd1f6f9e |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>8 files changed, 626 insertions(+), 26 deletions(-)
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+230-1| ... | @@ -55,7 +55,10 @@ import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; | ... | @@ -55,7 +55,10 @@ import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; |
| 55 | import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; | 55 | import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; |
| 56 | import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts"; | 56 | import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts"; |
| 57 | import * as ThreadPlanProgress from "../ThreadPlanProgress.ts"; | 57 | import * as ThreadPlanProgress from "../ThreadPlanProgress.ts"; |
| 58 | import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts"; | 58 | import { |
| 59 | ProviderRuntimeIngestionLive, | ||
| 60 | splitBufferedAssistantText, | ||
| 61 | } from "./ProviderRuntimeIngestion.ts"; | ||
| 59 | import { DEFAULT_THREAD_TITLE } from "../threadTitles.ts"; | 62 | import { DEFAULT_THREAD_TITLE } from "../threadTitles.ts"; |
| 60 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; | 63 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; |
| 61 | import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; | 64 | import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; |
| ... | @@ -297,7 +300,24 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -297,7 +300,24 @@ describe("ProviderRuntimeIngestion", () => { |
| 297 | }); | 300 | }); |
| 298 | }), | 301 | }), |
| 299 | ).pipe(Layer.provide(projectionSnapshotLayer)); | 302 | ).pipe(Layer.provide(projectionSnapshotLayer)); |
| 303 | // Real clock plus an offset the test can advance, so delivery pacing in | ||
| 304 | // ingestion can be driven without sleeping. Sleeps stay real. | ||
| 305 | let clockOffsetMs = 0; | ||
| 306 | const realClock = Effect.runSync(Effect.service(Clock.Clock)); | ||
| 307 | const shiftedClock: Clock.Clock = { | ||
| 308 | currentTimeMillisUnsafe: () => realClock.currentTimeMillisUnsafe() + clockOffsetMs, | ||
| 309 | currentTimeMillis: Effect.sync(() => realClock.currentTimeMillisUnsafe() + clockOffsetMs), | ||
| 310 | currentTimeNanosUnsafe: () => | ||
| 311 | realClock.currentTimeNanosUnsafe() + BigInt(clockOffsetMs) * 1_000_000n, | ||
| 312 | currentTimeNanos: Effect.sync( | ||
| 313 | () => realClock.currentTimeNanosUnsafe() + BigInt(clockOffsetMs) * 1_000_000n, | ||
| 314 | ), | ||
| 315 | monotonicTimeNanosUnsafe: () => realClock.monotonicTimeNanosUnsafe(), | ||
| 316 | monotonicTimeNanos: realClock.monotonicTimeNanos, | ||
| 317 | sleep: (duration) => realClock.sleep(duration), | ||
| 318 | }; | ||
| 300 | const layer = ProviderRuntimeIngestionLive.pipe( | 319 | const layer = ProviderRuntimeIngestionLive.pipe( |
| 320 | Layer.provide(Layer.succeed(Clock.Clock, shiftedClock)), | ||
| 301 | Layer.provideMerge(orchestrationLayer), | 321 | Layer.provideMerge(orchestrationLayer), |
| 302 | Layer.provideMerge(ingestionProjectionSnapshotLayer), | 322 | Layer.provideMerge(ingestionProjectionSnapshotLayer), |
| 303 | // Single shared liveness instance across ingestion (writer), the | 323 | // Single shared liveness instance across ingestion (writer), the |
| ... | @@ -392,6 +412,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -392,6 +412,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 392 | .pipe(Effect.map(Option.getOrThrow)), | 412 | .pipe(Effect.map(Option.getOrThrow)), |
| 393 | ), | 413 | ), |
| 394 | emit: provider.emit, | 414 | emit: provider.emit, |
| 415 | advanceClock: (ms: number) => { | ||
| 416 | clockOffsetMs += ms; | ||
| 417 | }, | ||
| 395 | emitAndDrain, | 418 | emitAndDrain, |
| 396 | sqlCount: sqlCounter.count, | 419 | sqlCount: sqlCounter.count, |
| 397 | setProviderSession: provider.setSession, | 420 | setProviderSession: provider.setSession, |
| ... | @@ -3129,6 +3152,145 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -3129,6 +3152,145 @@ describe("ProviderRuntimeIngestion", () => { |
| 3129 | expect(finalMessage?.streaming).toBe(false); | 3152 | expect(finalMessage?.streaming).toBe(false); |
| 3130 | }); | 3153 | }); |
| 3131 | 3154 | ||
| 3155 | it("delivers finished paragraphs while the rest of the message stays buffered", async () => { | ||
| 3156 | const harness = await createHarness(); | ||
| 3157 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 3158 | const codex = ProviderDriverKind.make("codex"); | ||
| 3159 | const threadId = asThreadId("thread-1"); | ||
| 3160 | const turnId = asTurnId("turn-paragraph-flush"); | ||
| 3161 | const itemId = asItemId("item-paragraph-flush"); | ||
| 3162 | |||
| 3163 | harness.emit({ | ||
| 3164 | type: "turn.started", | ||
| 3165 | eventId: asEventId("evt-paragraph-started"), | ||
| 3166 | provider: codex, | ||
| 3167 | createdAt: now, | ||
| 3168 | threadId, | ||
| 3169 | turnId, | ||
| 3170 | }); | ||
| 3171 | await waitForThread( | ||
| 3172 | harness.readModel, | ||
| 3173 | (thread) => thread.session?.status === "running" && thread.session?.activeTurnId === turnId, | ||
| 3174 | ); | ||
| 3175 | |||
| 3176 | // Each delta lands well outside the pacing window of the one before. | ||
| 3177 | const emitDelta = (eventId: string, delta: string) => { | ||
| 3178 | harness.advanceClock(1_000); | ||
| 3179 | harness.emit({ | ||
| 3180 | type: "content.delta", | ||
| 3181 | eventId: asEventId(eventId), | ||
| 3182 | provider: codex, | ||
| 3183 | createdAt: now, | ||
| 3184 | threadId, | ||
| 3185 | turnId, | ||
| 3186 | itemId, | ||
| 3187 | payload: { streamKind: "assistant_text", delta }, | ||
| 3188 | }); | ||
| 3189 | }; | ||
| 3190 | |||
| 3191 | emitDelta("evt-paragraph-1", "First paragraph.\n\nSecond para"); | ||
| 3192 | const afterFirst = await waitForThread(harness.readModel, (thread) => | ||
| 3193 | thread.messages.some( | ||
| 3194 | (message: ProviderRuntimeTestMessage) => message.id === `assistant:${itemId}`, | ||
| 3195 | ), | ||
| 3196 | ); | ||
| 3197 | expect( | ||
| 3198 | afterFirst.messages.find((m: ProviderRuntimeTestMessage) => m.id === `assistant:${itemId}`), | ||
| 3199 | ).toMatchObject({ | ||
| 3200 | text: "First paragraph.\n\n", | ||
| 3201 | streaming: true, | ||
| 3202 | }); | ||
| 3203 | |||
| 3204 | // An open code block holds the whole block until its closing fence lands. | ||
| 3205 | emitDelta("evt-paragraph-2", "graph.\n\n```ts\nconst a = 1;\n\nconst b = 2;\n"); | ||
| 3206 | await harness.drain(); | ||
| 3207 | expect( | ||
| 3208 | (await harness.readModel()).threads | ||
| 3209 | .find((t) => t.id === threadId) | ||
| 3210 | ?.messages.find((m: ProviderRuntimeTestMessage) => m.id === `assistant:${itemId}`)?.text, | ||
| 3211 | ).toBe("First paragraph.\n\nSecond paragraph.\n\n"); | ||
| 3212 | |||
| 3213 | emitDelta("evt-paragraph-3", "```\n\nTail without newline"); | ||
| 3214 | harness.emit({ | ||
| 3215 | type: "item.completed", | ||
| 3216 | eventId: asEventId("evt-paragraph-completed"), | ||
| 3217 | provider: codex, | ||
| 3218 | createdAt: now, | ||
| 3219 | threadId, | ||
| 3220 | turnId, | ||
| 3221 | itemId, | ||
| 3222 | payload: { itemType: "assistant_message", status: "completed" }, | ||
| 3223 | }); | ||
| 3224 | const finalThread = await waitForThread(harness.readModel, (thread) => | ||
| 3225 | thread.messages.some( | ||
| 3226 | (message: ProviderRuntimeTestMessage) => | ||
| 3227 | message.id === `assistant:${itemId}` && !message.streaming, | ||
| 3228 | ), | ||
| 3229 | ); | ||
| 3230 | expect( | ||
| 3231 | finalThread.messages.find((m: ProviderRuntimeTestMessage) => m.id === `assistant:${itemId}`) | ||
| 3232 | ?.text, | ||
| 3233 | ).toBe( | ||
| 3234 | "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nTail without newline", | ||
| 3235 | ); | ||
| 3236 | }); | ||
| 3237 | |||
| 3238 | it("holds paragraphs that finish inside the pacing window and lands them together", async () => { | ||
| 3239 | const harness = await createHarness(); | ||
| 3240 | const codex = ProviderDriverKind.make("codex"); | ||
| 3241 | const threadId = asThreadId("thread-1"); | ||
| 3242 | const turnId = asTurnId("turn-paced"); | ||
| 3243 | const itemId = asItemId("item-paced"); | ||
| 3244 | // Every delta carries the same event time, like OpenCode does for one | ||
| 3245 | // part. Pacing must follow the server clock, not the event stamp. | ||
| 3246 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 3247 | |||
| 3248 | harness.emit({ | ||
| 3249 | type: "turn.started", | ||
| 3250 | eventId: asEventId("evt-paced-started"), | ||
| 3251 | provider: codex, | ||
| 3252 | createdAt: now, | ||
| 3253 | threadId, | ||
| 3254 | turnId, | ||
| 3255 | }); | ||
| 3256 | await waitForThread( | ||
| 3257 | harness.readModel, | ||
| 3258 | (thread) => thread.session?.status === "running" && thread.session?.activeTurnId === turnId, | ||
| 3259 | ); | ||
| 3260 | // Emit is fire-and-forget, so drain after each delta before moving the | ||
| 3261 | // clock. Otherwise the worker reads a clock that has already advanced. | ||
| 3262 | let clockMs = 0; | ||
| 3263 | const emitDelta = async (eventId: string, delta: string, offsetMs: number) => { | ||
| 3264 | harness.advanceClock(offsetMs - clockMs); | ||
| 3265 | clockMs = offsetMs; | ||
| 3266 | await harness.emitAndDrain([ | ||
| 3267 | { | ||
| 3268 | type: "content.delta", | ||
| 3269 | eventId: asEventId(eventId), | ||
| 3270 | provider: codex, | ||
| 3271 | createdAt: now, | ||
| 3272 | threadId, | ||
| 3273 | turnId, | ||
| 3274 | itemId, | ||
| 3275 | payload: { streamKind: "assistant_text", delta }, | ||
| 3276 | }, | ||
| 3277 | ]); | ||
| 3278 | }; | ||
| 3279 | const messageText = async () => | ||
| 3280 | (await harness.readModel()).threads | ||
| 3281 | .find((t) => t.id === threadId) | ||
| 3282 | ?.messages.find((m: ProviderRuntimeTestMessage) => m.id === `assistant:${itemId}`)?.text; | ||
| 3283 | |||
| 3284 | await emitDelta("evt-paced-1", "One.\n\n", 0); | ||
| 3285 | await emitDelta("evt-paced-2", "Two.\n\n", 100); | ||
| 3286 | await emitDelta("evt-paced-3", "Three.\n\n", 200); | ||
| 3287 | // The first paragraph lands right away. The next two are inside the window. | ||
| 3288 | expect(await messageText()).toBe("One.\n\n"); | ||
| 3289 | |||
| 3290 | await emitDelta("evt-paced-4", "Four.\n\n", 500); | ||
| 3291 | expect(await messageText()).toBe("One.\n\nTwo.\n\nThree.\n\nFour.\n\n"); | ||
| 3292 | }); | ||
| 3293 | |||
| 3132 | it("spills oversized buffered deltas and still finalizes full assistant text", async () => { | 3294 | it("spills oversized buffered deltas and still finalizes full assistant text", async () => { |
| 3133 | const harness = await createHarness(); | 3295 | const harness = await createHarness(); |
| 3134 | const now = "2026-01-01T00:00:00.000Z"; | 3296 | const now = "2026-01-01T00:00:00.000Z"; |
| ... | @@ -4367,3 +4529,70 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -4367,3 +4529,70 @@ describe("ProviderRuntimeIngestion", () => { |
| 4367 | expect(thread.session?.lastError).toBe("runtime still processed"); | 4529 | expect(thread.session?.lastError).toBe("runtime still processed"); |
| 4368 | }); | 4530 | }); |
| 4369 | }); | 4531 | }); |
| 4532 | |||
| 4533 | describe("splitBufferedAssistantText", () => { | ||
| 4534 | it("keeps a partial trailing line buffered", () => { | ||
| 4535 | expect(splitBufferedAssistantText("one\n\ntwo")).toEqual({ ready: "one\n\n", rest: "two" }); | ||
| 4536 | expect(splitBufferedAssistantText("one\ntwo")).toEqual({ ready: "", rest: "one\ntwo" }); | ||
| 4537 | }); | ||
| 4538 | |||
| 4539 | it("does not split inside an open fence and delivers the block at its closing fence", () => { | ||
| 4540 | const open = "intro\n\n```\ncode\n\nmore\n"; | ||
| 4541 | expect(splitBufferedAssistantText(open)).toEqual({ | ||
| 4542 | ready: "intro\n\n", | ||
| 4543 | rest: "```\ncode\n\nmore\n", | ||
| 4544 | }); | ||
| 4545 | expect(splitBufferedAssistantText(`${open}\`\`\`\nafter`)).toEqual({ | ||
| 4546 | ready: `${open}\`\`\`\n`, | ||
| 4547 | rest: "after", | ||
| 4548 | }); | ||
| 4549 | }); | ||
| 4550 | |||
| 4551 | it("does not treat a fence with an info string as a closing fence", () => { | ||
| 4552 | const text = "```\n```javascript\nstill code\n\nmore\n"; | ||
| 4553 | expect(splitBufferedAssistantText(text)).toEqual({ ready: "", rest: text }); | ||
| 4554 | }); | ||
| 4555 | |||
| 4556 | it("treats a fence indented four or more spaces as code, not a closing fence", () => { | ||
| 4557 | const text = "```\n ```\n\nstill code\n"; | ||
| 4558 | expect(splitBufferedAssistantText(text)).toEqual({ ready: "", rest: text }); | ||
| 4559 | expect(splitBufferedAssistantText("```\n ```\nafter")).toEqual({ | ||
| 4560 | ready: "```\n ```\n", | ||
| 4561 | rest: "after", | ||
| 4562 | }); | ||
| 4563 | }); | ||
| 4564 | |||
| 4565 | it("keeps a fence nested under a list item open across its blank lines", () => { | ||
| 4566 | const text = "- step\n\n ```ts\n a\n\n b\n ```\n\nafter\n"; | ||
| 4567 | expect(splitBufferedAssistantText(text)).toEqual({ | ||
| 4568 | ready: "- step\n\n ```ts\n a\n\n b\n ```\n\n", | ||
| 4569 | rest: "after\n", | ||
| 4570 | }); | ||
| 4571 | }); | ||
| 4572 | |||
| 4573 | it("does not treat a no-break-space line as blank", () => { | ||
| 4574 | expect(splitBufferedAssistantText("para\n\u00a0\ncont\n\nnext")).toEqual({ | ||
| 4575 | ready: "para\n\u00a0\ncont\n\n", | ||
| 4576 | rest: "next", | ||
| 4577 | }); | ||
| 4578 | }); | ||
| 4579 | |||
| 4580 | it("treats CRLF blank lines as boundaries", () => { | ||
| 4581 | expect(splitBufferedAssistantText("one\r\n\r\ntwo")).toEqual({ | ||
| 4582 | ready: "one\r\n\r\n", | ||
| 4583 | rest: "two", | ||
| 4584 | }); | ||
| 4585 | }); | ||
| 4586 | |||
| 4587 | it("only closes a fence with the same marker of equal or greater length", () => { | ||
| 4588 | const text = "````\n```\nstill code\n\n````\n\nout\n"; | ||
| 4589 | expect(splitBufferedAssistantText(text)).toEqual({ | ||
| 4590 | ready: "````\n```\nstill code\n\n````\n\n", | ||
| 4591 | rest: "out\n", | ||
| 4592 | }); | ||
| 4593 | expect(splitBufferedAssistantText("~~~\n```\n\nx\n")).toEqual({ | ||
| 4594 | ready: "", | ||
| 4595 | rest: "~~~\n```\n\nx\n", | ||
| 4596 | }); | ||
| 4597 | }); | ||
| 4598 | }); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+99-3| ... | @@ -19,6 +19,7 @@ import { | ... | @@ -19,6 +19,7 @@ import { |
| 19 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 20 | import * as Cache from "effect/Cache"; | 20 | import * as Cache from "effect/Cache"; |
| 21 | import * as Cause from "effect/Cause"; | 21 | import * as Cause from "effect/Cause"; |
| 22 | import * as Clock from "effect/Clock"; | ||
| 22 | import * as Crypto from "effect/Crypto"; | 23 | import * as Crypto from "effect/Crypto"; |
| 23 | import * as DateTime from "effect/DateTime"; | 24 | import * as DateTime from "effect/DateTime"; |
| 24 | import * as Duration from "effect/Duration"; | 25 | import * as Duration from "effect/Duration"; |
| ... | @@ -107,6 +108,11 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120); | ... | @@ -107,6 +108,11 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120); |
| 107 | const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000; | 108 | const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000; |
| 108 | const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120); | 109 | const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120); |
| 109 | const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; | 110 | const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; |
| 111 | // Paragraphs that finish within this window after a delivery stay buffered | ||
| 112 | // and land together on the next one. Keeps fast models from repainting the | ||
| 113 | // message several times a second while still showing the first paragraph | ||
| 114 | // as soon as it is done. | ||
| 115 | const MIN_ASSISTANT_DELIVERY_INTERVAL_MS = 400; | ||
| 110 | const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; | 116 | const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; |
| 111 | 117 | ||
| 112 | type TurnStartRequestedDomainEvent = Extract< | 118 | type TurnStartRequestedDomainEvent = Extract< |
| ... | @@ -180,6 +186,58 @@ function hasRenderableAssistantText(text: string | undefined): boolean { | ... | @@ -180,6 +186,58 @@ function hasRenderableAssistantText(text: string | undefined): boolean { |
| 180 | return (text?.trim().length ?? 0) > 0; | 186 | return (text?.trim().length ?? 0) > 0; |
| 181 | } | 187 | } |
| 182 | 188 | ||
| 189 | // An opening fence may sit at any indentation, since fences inside list | ||
| 190 | // items are indented past the marker. A closing fence may be indented at most | ||
| 191 | // three spaces more than its opener. Deeper lines are content in the block. | ||
| 192 | const MARKDOWN_FENCE_PATTERN = /^( *)(`{3,}|~{3,})/; | ||
| 193 | // CommonMark blank lines hold only spaces and tabs. Other whitespace, such as | ||
| 194 | // a no-break space, is paragraph content. | ||
| 195 | const BLANK_LINE_PATTERN = /^[ \t]*$/; | ||
| 196 | |||
| 197 | /** | ||
| 198 | * Splits buffered assistant text at the last blank line or closing code fence | ||
| 199 | * that is not inside an open fenced code block. `ready` is safe to deliver now | ||
| 200 | * because the markdown before it will not change shape as more text arrives. | ||
| 201 | * `rest` stays buffered until the next boundary or completion. Only fully | ||
| 202 | * terminated lines count, so a trailing partial line never leaks. | ||
| 203 | */ | ||
| 204 | export function splitBufferedAssistantText(text: string): { ready: string; rest: string } { | ||
| 205 | let openFence: { marker: string; indent: number } | null = null; | ||
| 206 | let boundary = -1; | ||
| 207 | let lineStart = 0; | ||
| 208 | for (;;) { | ||
| 209 | const newline = text.indexOf("\n", lineStart); | ||
| 210 | if (newline === -1) { | ||
| 211 | break; | ||
| 212 | } | ||
| 213 | const line = text.slice(lineStart, newline).replace(/[ \t\r]+$/, ""); | ||
| 214 | const fenceMatch = MARKDOWN_FENCE_PATTERN.exec(line); | ||
| 215 | if (fenceMatch) { | ||
| 216 | const indent = fenceMatch[1]!.length; | ||
| 217 | const marker = fenceMatch[2]!; | ||
| 218 | if (openFence === null) { | ||
| 219 | openFence = { marker, indent }; | ||
| 220 | } else if ( | ||
| 221 | marker[0] === openFence.marker[0] && | ||
| 222 | marker.length >= openFence.marker.length && | ||
| 223 | indent <= openFence.indent + 3 && | ||
| 224 | line.length === indent + marker.length | ||
| 225 | ) { | ||
| 226 | // CommonMark: a closing fence carries no info string. | ||
| 227 | openFence = null; | ||
| 228 | boundary = newline + 1; | ||
| 229 | } | ||
| 230 | } else if (openFence === null && BLANK_LINE_PATTERN.test(line) && lineStart > 0) { | ||
| 231 | boundary = newline + 1; | ||
| 232 | } | ||
| 233 | lineStart = newline + 1; | ||
| 234 | } | ||
| 235 | if (boundary === -1) { | ||
| 236 | return { ready: "", rest: text }; | ||
| 237 | } | ||
| 238 | return { ready: text.slice(0, boundary), rest: text.slice(boundary) }; | ||
| 239 | } | ||
| 240 | |||
| 183 | function proposedPlanIdForTurn(threadId: ThreadId, turnId: TurnId): string { | 241 | function proposedPlanIdForTurn(threadId: ThreadId, turnId: TurnId): string { |
| 184 | return `plan:${threadId}:turn:${turnId}`; | 242 | return `plan:${threadId}:turn:${turnId}`; |
| 185 | } | 243 | } |
| ... | @@ -927,6 +985,12 @@ const make = Effect.gen(function* () { | ... | @@ -927,6 +985,12 @@ const make = Effect.gen(function* () { |
| 927 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, | 985 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, |
| 928 | lookup: () => Effect.succeed(""), | 986 | lookup: () => Effect.succeed(""), |
| 929 | }); | 987 | }); |
| 988 | // Epoch millis of the last early delivery per message, for pacing. | ||
| 989 | const lastAssistantDeliveryAtByMessageId = yield* Cache.make<MessageId, number>({ | ||
| 990 | capacity: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY, | ||
| 991 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, | ||
| 992 | lookup: () => Effect.succeed(0), | ||
| 993 | }); | ||
| 930 | 994 | ||
| 931 | const assistantSegmentStateByTurnKey = yield* Cache.make<string, AssistantSegmentState>({ | 995 | const assistantSegmentStateByTurnKey = yield* Cache.make<string, AssistantSegmentState>({ |
| 932 | capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, | 996 | capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, |
| ... | @@ -1103,7 +1167,7 @@ const make = Effect.gen(function* () { | ... | @@ -1103,7 +1167,7 @@ const make = Effect.gen(function* () { |
| 1103 | }); | 1167 | }); |
| 1104 | }); | 1168 | }); |
| 1105 | 1169 | ||
| 1106 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => | 1170 | const appendBufferedAssistantText = (messageId: MessageId, delta: string, atMillis: number) => |
| 1107 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( | 1171 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| 1108 | Effect.flatMap((existingText) => | 1172 | Effect.flatMap((existingText) => |
| 1109 | Effect.gen(function* () { | 1173 | Effect.gen(function* () { |
| ... | @@ -1111,6 +1175,30 @@ const make = Effect.gen(function* () { | ... | @@ -1111,6 +1175,30 @@ const make = Effect.gen(function* () { |
| 1111 | onNone: () => delta, | 1175 | onNone: () => delta, |
| 1112 | onSome: (text) => `${text}${delta}`, | 1176 | onSome: (text) => `${text}${delta}`, |
| 1113 | }); | 1177 | }); |
| 1178 | |||
| 1179 | // Deliver finished paragraphs and closed code blocks early so the | ||
| 1180 | // user sees progress without token-by-token repaints. | ||
| 1181 | const { ready, rest } = splitBufferedAssistantText(nextText); | ||
| 1182 | const lastDeliveredAt = Option.getOrUndefined( | ||
| 1183 | yield* Cache.getOption(lastAssistantDeliveryAtByMessageId, messageId), | ||
| 1184 | ); | ||
| 1185 | const paced = | ||
| 1186 | lastDeliveredAt === undefined || | ||
| 1187 | atMillis - lastDeliveredAt >= MIN_ASSISTANT_DELIVERY_INTERVAL_MS; | ||
| 1188 | if ( | ||
| 1189 | paced && | ||
| 1190 | hasRenderableAssistantText(ready) && | ||
| 1191 | rest.length <= MAX_BUFFERED_ASSISTANT_CHARS | ||
| 1192 | ) { | ||
| 1193 | if (rest.length > 0) { | ||
| 1194 | yield* Cache.set(bufferedAssistantTextByMessageId, messageId, rest); | ||
| 1195 | } else { | ||
| 1196 | yield* Cache.invalidate(bufferedAssistantTextByMessageId, messageId); | ||
| 1197 | } | ||
| 1198 | yield* Cache.set(lastAssistantDeliveryAtByMessageId, messageId, atMillis); | ||
| 1199 | return ready; | ||
| 1200 | } | ||
| 1201 | |||
| 1114 | if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) { | 1202 | if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) { |
| 1115 | yield* Cache.set(bufferedAssistantTextByMessageId, messageId, nextText); | 1203 | yield* Cache.set(bufferedAssistantTextByMessageId, messageId, nextText); |
| 1116 | return ""; | 1204 | return ""; |
| ... | @@ -1133,7 +1221,9 @@ const make = Effect.gen(function* () { | ... | @@ -1133,7 +1221,9 @@ const make = Effect.gen(function* () { |
| 1133 | ); | 1221 | ); |
| 1134 | 1222 | ||
| 1135 | const clearBufferedAssistantText = (messageId: MessageId) => | 1223 | const clearBufferedAssistantText = (messageId: MessageId) => |
| 1136 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId); | 1224 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId).pipe( |
| 1225 | Effect.andThen(Cache.invalidate(lastAssistantDeliveryAtByMessageId, messageId)), | ||
| 1226 | ); | ||
| 1137 | 1227 | ||
| 1138 | const appendBufferedProposedPlan = (planId: string, delta: string, createdAt: string) => | 1228 | const appendBufferedProposedPlan = (planId: string, delta: string, createdAt: string) => |
| 1139 | Cache.getOption(bufferedProposedPlanById, planId).pipe( | 1229 | Cache.getOption(bufferedProposedPlanById, planId).pipe( |
| ... | @@ -1675,7 +1765,13 @@ const make = Effect.gen(function* () { | ... | @@ -1675,7 +1765,13 @@ const make = Effect.gen(function* () { |
| 1675 | : "buffered", | 1765 | : "buffered", |
| 1676 | ); | 1766 | ); |
| 1677 | if (assistantDeliveryMode === "buffered") { | 1767 | if (assistantDeliveryMode === "buffered") { |
| 1678 | const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta); | 1768 | // Pace on the server clock. OpenCode stamps every delta of a part |
| 1769 | // with the part's start time, so the event time cannot measure gaps. | ||
| 1770 | const spillChunk = yield* appendBufferedAssistantText( | ||
| 1771 | assistantMessageId, | ||
| 1772 | assistantDelta, | ||
| 1773 | yield* Clock.currentTimeMillis, | ||
| 1774 | ); | ||
| 1679 | if (spillChunk.length > 0) { | 1775 | if (spillChunk.length > 0) { |
| 1680 | yield* orchestrationEngine.dispatch({ | 1776 | yield* orchestrationEngine.dispatch({ |
| 1681 | type: "thread.message.assistant.delta", | 1777 | type: "thread.message.assistant.delta", |
apps/web/src/components/ChatMarkdown.tsx+11-1| ... | @@ -3196,7 +3196,15 @@ const CHAT_MARKDOWN_COMPONENTS = { | ... | @@ -3196,7 +3196,15 @@ const CHAT_MARKDOWN_COMPONENTS = { |
| 3196 | resetKeys={[codeBlock.code, language, diffThemeName, isStreaming]} | 3196 | resetKeys={[codeBlock.code, language, diffThemeName, isStreaming]} |
| 3197 | fallback={<pre {...props}>{children}</pre>} | 3197 | fallback={<pre {...props}>{children}</pre>} |
| 3198 | > | 3198 | > |
| 3199 | <Suspense fallback={<pre {...props}>{children}</pre>}> | 3199 | {/* Reserve the block's height but stay hidden until Shiki has colored |
| 3200 | it, so plain text never flashes before the highlighted version. */} | ||
| 3201 | <Suspense | ||
| 3202 | fallback={ | ||
| 3203 | <pre {...props} className="invisible" aria-hidden> | ||
| 3204 | {children} | ||
| 3205 | </pre> | ||
| 3206 | } | ||
| 3207 | > | ||
| 3200 | <SuspenseShikiCodeBlock | 3208 | <SuspenseShikiCodeBlock |
| 3201 | className={codeBlock.className} | 3209 | className={codeBlock.className} |
| 3202 | code={codeBlock.code} | 3210 | code={codeBlock.code} |
| ... | @@ -3249,6 +3257,8 @@ function ChatMarkdown({ | ... | @@ -3249,6 +3257,8 @@ function ChatMarkdown({ |
| 3249 | "chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80 [overflow-wrap:anywhere] [word-break:break-word]", | 3257 | "chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80 [overflow-wrap:anywhere] [word-break:break-word]", |
| 3250 | className, | 3258 | className, |
| 3251 | )} | 3259 | )} |
| 3260 | // Gates the fade-in for blocks that arrive while the response streams. | ||
| 3261 | data-streaming={componentState.isStreaming ? "" : undefined} | ||
| 3252 | onCopy={handleCopy} | 3262 | onCopy={handleCopy} |
| 3253 | > | 3263 | > |
| 3254 | <ChatMarkdownRendererContext value={componentState}> | 3264 | <ChatMarkdownRendererContext value={componentState}> |
apps/web/src/components/chat/MessagesTimeline.test.tsx+87| ... | @@ -901,6 +901,93 @@ describe("MessagesTimeline", () => { | ... | @@ -901,6 +901,93 @@ describe("MessagesTimeline", () => { |
| 901 | expect(markup).not.toContain("<a href="); | 901 | expect(markup).not.toContain("<a href="); |
| 902 | }); | 902 | }); |
| 903 | 903 | ||
| 904 | it("glides to the end while a turn is running and snaps otherwise", () => { | ||
| 905 | const entries = [buildUserTimelineEntry("Hello")]; | ||
| 906 | const working = renderToStaticMarkup( | ||
| 907 | <MessagesTimeline {...buildProps()} isWorking timelineEntries={entries} />, | ||
| 908 | ); | ||
| 909 | expect(working).toContain('data-maintain-scroll-at-end-animated="true"'); | ||
| 910 | |||
| 911 | const idle = renderToStaticMarkup( | ||
| 912 | <MessagesTimeline {...buildProps()} timelineEntries={entries} />, | ||
| 913 | ); | ||
| 914 | expect(idle).toContain('data-maintain-scroll-at-end-animated="false"'); | ||
| 915 | }); | ||
| 916 | |||
| 917 | it("snaps to the end while a thread switch settles, even mid-turn", async () => { | ||
| 918 | const frames = new Map<number, FrameRequestCallback>(); | ||
| 919 | let nextFrame = 0; | ||
| 920 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { | ||
| 921 | frames.set(++nextFrame, callback); | ||
| 922 | return nextFrame; | ||
| 923 | }); | ||
| 924 | vi.stubGlobal("cancelAnimationFrame", (frame: number) => frames.delete(frame)); | ||
| 925 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | ||
| 926 | const flushFrame = () => | ||
| 927 | act(() => { | ||
| 928 | const callbacks = [...frames.values()]; | ||
| 929 | frames.clear(); | ||
| 930 | callbacks.forEach((callback) => callback(0)); | ||
| 931 | }); | ||
| 932 | // A work entry renders without the DOM globals that message rows need | ||
| 933 | // under react-test-renderer. | ||
| 934 | const entries = [ | ||
| 935 | { | ||
| 936 | id: "entry-settle-work", | ||
| 937 | kind: "work" as const, | ||
| 938 | createdAt: MESSAGE_CREATED_AT, | ||
| 939 | entry: { | ||
| 940 | id: "work-settle", | ||
| 941 | createdAt: MESSAGE_CREATED_AT, | ||
| 942 | toolCallId: "call-settle", | ||
| 943 | label: "Run lint", | ||
| 944 | tone: "tool" as const, | ||
| 945 | itemType: "command_execution" as const, | ||
| 946 | command: "pnpm lint", | ||
| 947 | toolLifecycleStatus: "completed" as const, | ||
| 948 | }, | ||
| 949 | }, | ||
| 950 | ]; | ||
| 951 | const animatedAttr = (renderer: ReactTestRenderer) => | ||
| 952 | renderer.root.findByProps({ "data-testid": "legend-list" }).props[ | ||
| 953 | "data-maintain-scroll-at-end-animated" | ||
| 954 | ]; | ||
| 955 | let renderer!: ReactTestRenderer; | ||
| 956 | try { | ||
| 957 | act(() => { | ||
| 958 | renderer = create( | ||
| 959 | <MessagesTimeline | ||
| 960 | {...buildProps()} | ||
| 961 | isWorking | ||
| 962 | routeThreadKey="env-1:thread-a" | ||
| 963 | timelineEntries={entries} | ||
| 964 | />, | ||
| 965 | ); | ||
| 966 | }); | ||
| 967 | expect(animatedAttr(renderer)).toBe(true); | ||
| 968 | |||
| 969 | act(() => { | ||
| 970 | renderer.update( | ||
| 971 | <MessagesTimeline | ||
| 972 | {...buildProps()} | ||
| 973 | isWorking | ||
| 974 | routeThreadKey="env-1:thread-b" | ||
| 975 | timelineEntries={entries} | ||
| 976 | />, | ||
| 977 | ); | ||
| 978 | }); | ||
| 979 | expect(animatedAttr(renderer)).toBe(false); | ||
| 980 | |||
| 981 | // Two frames later the switch has settled and gliding resumes. | ||
| 982 | flushFrame(); | ||
| 983 | flushFrame(); | ||
| 984 | expect(animatedAttr(renderer)).toBe(true); | ||
| 985 | } finally { | ||
| 986 | act(() => renderer?.unmount()); | ||
| 987 | vi.unstubAllGlobals(); | ||
| 988 | } | ||
| 989 | }); | ||
| 990 | |||
| 904 | it("keeps reserved end space when tool work starts while reading history", () => { | 991 | it("keeps reserved end space when tool work starts while reading history", () => { |
| 905 | const turnId = TurnId.make("turn-with-active-tool"); | 992 | const turnId = TurnId.make("turn-with-active-tool"); |
| 906 | const firstEntry = buildUserTimelineEntry("Run the command."); | 993 | const firstEntry = buildUserTimelineEntry("Run the command."); |
apps/web/src/components/chat/MessagesTimeline.tsx+31-1| ... | @@ -227,6 +227,7 @@ import { | ... | @@ -227,6 +227,7 @@ import { |
| 227 | import { createContextPresentationRegistry } from "../contextPresentationRegistry"; | 227 | import { createContextPresentationRegistry } from "../contextPresentationRegistry"; |
| 228 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; | 228 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; |
| 229 | import type { ChatMarkdownContextReference } from "../ChatMarkdown"; | 229 | import type { ChatMarkdownContextReference } from "../ChatMarkdown"; |
| 230 | import { useMediaQuery } from "~/hooks/useMediaQuery"; | ||
| 230 | import { cn } from "~/lib/utils"; | 231 | import { cn } from "~/lib/utils"; |
| 231 | import { useUiStateStore } from "~/uiStateStore"; | 232 | import { useUiStateStore } from "~/uiStateStore"; |
| 232 | import { type TimestampFormat } from "@t3tools/contracts/settings"; | 233 | import { type TimestampFormat } from "@t3tools/contracts/settings"; |
| ... | @@ -348,6 +349,13 @@ const TIMELINE_MAINTAIN_SCROLL_AT_END = { | ... | @@ -348,6 +349,13 @@ const TIMELINE_MAINTAIN_SCROLL_AT_END = { |
| 348 | layout: true, | 349 | layout: true, |
| 349 | }, | 350 | }, |
| 350 | } as const satisfies MaintainScrollAtEndOptions; | 351 | } as const satisfies MaintainScrollAtEndOptions; |
| 352 | // Streamed text lands a paragraph at a time. A smooth scroll to the end | ||
| 353 | // turns each landing into a short glide instead of a jump. Thread switches | ||
| 354 | // and layout settles keep the instant variant so nothing visibly travels. | ||
| 355 | const TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH = { | ||
| 356 | ...TIMELINE_MAINTAIN_SCROLL_AT_END, | ||
| 357 | animated: true, | ||
| 358 | } as const satisfies MaintainScrollAtEndOptions; | ||
| 351 | 359 | ||
| 352 | // --------------------------------------------------------------------------- | 360 | // --------------------------------------------------------------------------- |
| 353 | // Props (public API) | 361 | // Props (public API) |
| ... | @@ -470,14 +478,19 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -470,14 +478,19 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 470 | new Set(), | 478 | new Set(), |
| 471 | ); | 479 | ); |
| 472 | const listIdentityKey = displayThreadKey ?? routeThreadKey; | 480 | const listIdentityKey = displayThreadKey ?? routeThreadKey; |
| 481 | const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); | ||
| 473 | const listIdentityRef = useRef(listIdentityKey); | 482 | const listIdentityRef = useRef(listIdentityKey); |
| 474 | const previousLatestTurnRef = useRef(latestTurn); | 483 | const previousLatestTurnRef = useRef(latestTurn); |
| 484 | // The list stays mounted across thread switches. Its first end pins on the | ||
| 485 | // new thread must snap, not glide, even if that thread is mid-turn. | ||
| 486 | const [settlingListIdentity, setSettlingListIdentity] = useState<string | null>(null); | ||
| 475 | let paintedExpandedTurnIds = expandedTurnIds; | 487 | let paintedExpandedTurnIds = expandedTurnIds; |
| 476 | let paintedExpandedWorkGroupIds = expandedWorkGroupIds; | 488 | let paintedExpandedWorkGroupIds = expandedWorkGroupIds; |
| 477 | let paintedExpandedSpawnEntryIds = expandedSpawnEntryIds; | 489 | let paintedExpandedSpawnEntryIds = expandedSpawnEntryIds; |
| 478 | if (listIdentityRef.current !== listIdentityKey) { | 490 | if (listIdentityRef.current !== listIdentityKey) { |
| 479 | listIdentityRef.current = listIdentityKey; | 491 | listIdentityRef.current = listIdentityKey; |
| 480 | previousLatestTurnRef.current = latestTurn; | 492 | previousLatestTurnRef.current = latestTurn; |
| 493 | setSettlingListIdentity(listIdentityKey); | ||
| 481 | paintedExpandedTurnIds = new Set(); | 494 | paintedExpandedTurnIds = new Set(); |
| 482 | paintedExpandedWorkGroupIds = new Set(); | 495 | paintedExpandedWorkGroupIds = new Set(); |
| 483 | paintedExpandedSpawnEntryIds = new Set(); | 496 | paintedExpandedSpawnEntryIds = new Set(); |
| ... | @@ -522,6 +535,21 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -522,6 +535,21 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 522 | }; | 535 | }; |
| 523 | }, []); | 536 | }, []); |
| 524 | 537 | ||
| 538 | useEffect(() => { | ||
| 539 | if (settlingListIdentity === null) return; | ||
| 540 | // Two frames covers the fresh-data layout pass and the initial end pin. | ||
| 541 | let second: number | null = null; | ||
| 542 | const first = requestAnimationFrame(() => { | ||
| 543 | second = requestAnimationFrame(() => { | ||
| 544 | setSettlingListIdentity((current) => (current === settlingListIdentity ? null : current)); | ||
| 545 | }); | ||
| 546 | }); | ||
| 547 | return () => { | ||
| 548 | cancelAnimationFrame(first); | ||
| 549 | if (second !== null) cancelAnimationFrame(second); | ||
| 550 | }; | ||
| 551 | }, [settlingListIdentity]); | ||
| 552 | |||
| 525 | const suspendEndScrollMaintenanceForDisclosure = useCallback( | 553 | const suspendEndScrollMaintenanceForDisclosure = useCallback( |
| 526 | (anchorKey: string, collapsed = false) => { | 554 | (anchorKey: string, collapsed = false) => { |
| 527 | disclosureAnchorKeyRef.current = anchorKey; | 555 | disclosureAnchorKeyRef.current = anchorKey; |
| ... | @@ -977,7 +1005,9 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -977,7 +1005,9 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 977 | !liveFollowEnabled || | 1005 | !liveFollowEnabled || |
| 978 | disclosureToggleSettling | 1006 | disclosureToggleSettling |
| 979 | ? false | 1007 | ? false |
| 980 | : TIMELINE_MAINTAIN_SCROLL_AT_END | 1008 | : isWorking && !prefersReducedMotion && settlingListIdentity === null |
| 1009 | ? TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH | ||
| 1010 | : TIMELINE_MAINTAIN_SCROLL_AT_END | ||
| 981 | } | 1011 | } |
| 982 | maintainVisibleContentPosition={ | 1012 | maintainVisibleContentPosition={ |
| 983 | citationPositioning ? false : maintainVisibleContentPosition | 1013 | citationPositioning ? false : maintainVisibleContentPosition |
apps/web/src/index.css+16| ... | @@ -1891,6 +1891,22 @@ code { | ... | @@ -1891,6 +1891,22 @@ code { |
| 1891 | background: transparent !important; | 1891 | background: transparent !important; |
| 1892 | } | 1892 | } |
| 1893 | 1893 | ||
| 1894 | /* Paragraphs and code blocks arrive in chunks while a response streams. Fade | ||
| 1895 | each new block in so the chunk does not pop. @starting-style only applies | ||
| 1896 | when an element is first inserted, and the rule is gated on data-streaming, | ||
| 1897 | so opening a finished thread never replays the fade. Opacity only, one | ||
| 1898 | shot, no layout change. */ | ||
| 1899 | @media (prefers-reduced-motion: no-preference) { | ||
| 1900 | .chat-markdown[data-streaming] > *, | ||
| 1901 | .chat-markdown[data-streaming] .chat-markdown-shiki { | ||
| 1902 | transition: opacity 600ms ease-out; | ||
| 1903 | |||
| 1904 | @starting-style { | ||
| 1905 | opacity: 0; | ||
| 1906 | } | ||
| 1907 | } | ||
| 1908 | } | ||
| 1909 | |||
| 1894 | /* Diagnostics-style tables: row separators only, uppercase headers, and a | 1910 | /* Diagnostics-style tables: row separators only, uppercase headers, and a |
| 1895 | scroll-fade container for horizontal overflow. The root chat-markdown | 1911 | scroll-fade container for horizontal overflow. The root chat-markdown |
| 1896 | wrapping rules (overflow-wrap: anywhere) would let columns shrink to single | 1912 | wrapping rules (overflow-wrap: anywhere) would let columns shrink to single |
patches/@legendapp__list@3.3.5.patch+147-15| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts | 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts |
| 2 | index 367945cdfa8a8c260b7a127657a75c016c9ab46f..0ac7bf8b8e9c386058b2374199e58fe8c92d39b3 100644 | 2 | index 367945c..0ac7bf8 100644 |
| 3 | --- a/keyboard.d.ts | 3 | --- a/keyboard.d.ts |
| 4 | +++ b/keyboard.d.ts | 4 | +++ b/keyboard.d.ts |
| 5 | @@ -279,7 +279,7 @@ type KeyboardChatComposerInsetListRef = { | 5 | @@ -279,7 +279,7 @@ type KeyboardChatComposerInsetListRef = { |
| ... | @@ -23,7 +23,7 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..0ac7bf8b8e9c386058b2374199e58fe8 | ... | @@ -23,7 +23,7 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..0ac7bf8b8e9c386058b2374199e58fe8 |
| 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; | 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; |
| 24 | 24 | ||
| 25 | diff --git a/keyboard.js b/keyboard.js | 25 | diff --git a/keyboard.js b/keyboard.js |
| 26 | index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..321c4c305129dd3ce3f06127c47d51720a73fb83 100644 | 26 | index 6645bcb..321c4c3 100644 |
| 27 | --- a/keyboard.js | 27 | --- a/keyboard.js |
| 28 | +++ b/keyboard.js | 28 | +++ b/keyboard.js |
| 29 | @@ -33,19 +33,22 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. | 29 | @@ -33,19 +33,22 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. |
| ... | @@ -119,7 +119,7 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..321c4c305129dd3ce3f06127c47d5172 | ... | @@ -119,7 +119,7 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..321c4c305129dd3ce3f06127c47d5172 |
| 119 | renderScrollComponent: memoList, | 119 | renderScrollComponent: memoList, |
| 120 | ...rest | 120 | ...rest |
| 121 | diff --git a/keyboard.mjs b/keyboard.mjs | 121 | diff --git a/keyboard.mjs b/keyboard.mjs |
| 122 | index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..111eb242ba9d9ade1cb912550f7df85cf4f361ee 100644 | 122 | index 87b38b9..111eb24 100644 |
| 123 | --- a/keyboard.mjs | 123 | --- a/keyboard.mjs |
| 124 | +++ b/keyboard.mjs | 124 | +++ b/keyboard.mjs |
| 125 | @@ -1,7 +1,7 @@ | 125 | @@ -1,7 +1,7 @@ |
| ... | @@ -224,7 +224,7 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..111eb242ba9d9ade1cb912550f7df85c | ... | @@ -224,7 +224,7 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..111eb242ba9d9ade1cb912550f7df85c |
| 224 | renderScrollComponent: memoList, | 224 | renderScrollComponent: memoList, |
| 225 | ...rest | 225 | ...rest |
| 226 | diff --git a/react-native.d.ts b/react-native.d.ts | 226 | diff --git a/react-native.d.ts b/react-native.d.ts |
| 227 | index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6aba6636e5 100644 | 227 | index ce1fe00..3ccf6f1 100644 |
| 228 | --- a/react-native.d.ts | 228 | --- a/react-native.d.ts |
| 229 | +++ b/react-native.d.ts | 229 | +++ b/react-native.d.ts |
| 230 | @@ -293,6 +293,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | 230 | @@ -293,6 +293,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { |
| ... | @@ -241,13 +241,13 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a | ... | @@ -241,13 +241,13 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a |
| 241 | * Number of columns to render items in. | 241 | * Number of columns to render items in. |
| 242 | * @default 1 | 242 | * @default 1 |
| 243 | diff --git a/react-native.js b/react-native.js | 243 | diff --git a/react-native.js b/react-native.js |
| 244 | index b3c5a306b293f797a8b338adfca3060c0f6db22b..5ac6bbe8fe40bf252fefa6b885c2196d0677d75f 100644 | 244 | index b3c5a30..5ac6bbe 100644 |
| 245 | --- a/react-native.js | 245 | --- a/react-native.js |
| 246 | +++ b/react-native.js | 246 | +++ b/react-native.js |
| 247 | @@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) { | 247 | @@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) { |
| 248 | return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; | 248 | return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; |
| 249 | } | 249 | } |
| 250 | 250 | ||
| 251 | +// Size-only changes may not emit a scroll event to refresh the edge signal. | 251 | +// Size-only changes may not emit a scroll event to refresh the edge signal. |
| 252 | +function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) { | 252 | +function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) { |
| 253 | + const { queuedInitialLayout, scroll, scrollLength } = ctx.state; | 253 | + const { queuedInitialLayout, scroll, scrollLength } = ctx.state; |
| ... | @@ -809,13 +809,13 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..5ac6bbe8fe40bf252fefa6b885c2196d | ... | @@ -809,13 +809,13 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..5ac6bbe8fe40bf252fefa6b885c2196d |
| 809 | recycleItems, | 809 | recycleItems, |
| 810 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { | 810 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { |
| 811 | diff --git a/react-native.mjs b/react-native.mjs | 811 | diff --git a/react-native.mjs b/react-native.mjs |
| 812 | index 40e87cda8c9bc79a889e5542f29af429a24b24d4..93aac741d2cf77ce35996362439108176f19da7a 100644 | 812 | index 40e87cd..93aac74 100644 |
| 813 | --- a/react-native.mjs | 813 | --- a/react-native.mjs |
| 814 | +++ b/react-native.mjs | 814 | +++ b/react-native.mjs |
| 815 | @@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) { | 815 | @@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) { |
| 816 | return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; | 816 | return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; |
| 817 | } | 817 | } |
| 818 | 818 | ||
| 819 | +// Size-only changes may not emit a scroll event to refresh the edge signal. | 819 | +// Size-only changes may not emit a scroll event to refresh the edge signal. |
| 820 | +function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) { | 820 | +function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) { |
| 821 | + const { queuedInitialLayout, scroll, scrollLength } = ctx.state; | 821 | + const { queuedInitialLayout, scroll, scrollLength } = ctx.state; |
| ... | @@ -1376,8 +1376,78 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..93aac741d2cf77ce3599636243910817 | ... | @@ -1376,8 +1376,78 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..93aac741d2cf77ce3599636243910817 |
| 1376 | onScroll: onScrollHandler, | 1376 | onScroll: onScrollHandler, |
| 1377 | recycleItems, | 1377 | recycleItems, |
| 1378 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { | 1378 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { |
| 1379 | diff --git a/react-native.web.js b/react-native.web.js | ||
| 1380 | index 914d2da..8b88b09 100644 | ||
| 1381 | --- a/react-native.web.js | ||
| 1382 | +++ b/react-native.web.js | ||
| 1383 | @@ -5651,6 +5651,18 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) { | ||
| 1384 | }); | ||
| 1385 | |||
| 1386 | // src/utils/reordering.ts | ||
| 1387 | +// Element.moveBefore keeps the moved subtree's state and does not count as an | ||
| 1388 | +// insertion, so CSS @starting-style transitions and iframes inside a row do not | ||
| 1389 | +// restart when the list reorders its containers. insertBefore is the fallback. | ||
| 1390 | +function moveChildBefore(container, element, reference) { | ||
| 1391 | + if (typeof container.moveBefore === "function") { | ||
| 1392 | + container.moveBefore(element, reference); | ||
| 1393 | + } else if (reference) { | ||
| 1394 | + container.insertBefore(element, reference); | ||
| 1395 | + } else { | ||
| 1396 | + container.appendChild(element); | ||
| 1397 | + } | ||
| 1398 | +} | ||
| 1399 | function sortDOMElements(container, indexByElement) { | ||
| 1400 | const elements = Array.from(container.children); | ||
| 1401 | if (elements.length <= 1) return elements; | ||
| 1402 | @@ -5690,9 +5702,9 @@ function sortDOMElements(container, indexByElement) { | ||
| 1403 | } | ||
| 1404 | } | ||
| 1405 | if (nextStableElement) { | ||
| 1406 | - container.insertBefore(element, nextStableElement); | ||
| 1407 | + moveChildBefore(container, element, nextStableElement); | ||
| 1408 | } else { | ||
| 1409 | - container.appendChild(element); | ||
| 1410 | + moveChildBefore(container, element, null); | ||
| 1411 | } | ||
| 1412 | } | ||
| 1413 | } | ||
| 1414 | diff --git a/react-native.web.mjs b/react-native.web.mjs | ||
| 1415 | index 95465f2..d7cbe1c 100644 | ||
| 1416 | --- a/react-native.web.mjs | ||
| 1417 | +++ b/react-native.web.mjs | ||
| 1418 | @@ -5630,6 +5630,18 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) { | ||
| 1419 | }); | ||
| 1420 | |||
| 1421 | // src/utils/reordering.ts | ||
| 1422 | +// Element.moveBefore keeps the moved subtree's state and does not count as an | ||
| 1423 | +// insertion, so CSS @starting-style transitions and iframes inside a row do not | ||
| 1424 | +// restart when the list reorders its containers. insertBefore is the fallback. | ||
| 1425 | +function moveChildBefore(container, element, reference) { | ||
| 1426 | + if (typeof container.moveBefore === "function") { | ||
| 1427 | + container.moveBefore(element, reference); | ||
| 1428 | + } else if (reference) { | ||
| 1429 | + container.insertBefore(element, reference); | ||
| 1430 | + } else { | ||
| 1431 | + container.appendChild(element); | ||
| 1432 | + } | ||
| 1433 | +} | ||
| 1434 | function sortDOMElements(container, indexByElement) { | ||
| 1435 | const elements = Array.from(container.children); | ||
| 1436 | if (elements.length <= 1) return elements; | ||
| 1437 | @@ -5669,9 +5681,9 @@ function sortDOMElements(container, indexByElement) { | ||
| 1438 | } | ||
| 1439 | } | ||
| 1440 | if (nextStableElement) { | ||
| 1441 | - container.insertBefore(element, nextStableElement); | ||
| 1442 | + moveChildBefore(container, element, nextStableElement); | ||
| 1443 | } else { | ||
| 1444 | - container.appendChild(element); | ||
| 1445 | + moveChildBefore(container, element, null); | ||
| 1446 | } | ||
| 1447 | } | ||
| 1448 | } | ||
| 1379 | diff --git a/react.js b/react.js | 1449 | diff --git a/react.js b/react.js |
| 1380 | index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658458edbb1 100644 | 1450 | index 914d2da..0c2917d 100644 |
| 1381 | --- a/react.js | 1451 | --- a/react.js |
| 1382 | +++ b/react.js | 1452 | +++ b/react.js |
| 1383 | @@ -4702,7 +4702,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1453 | @@ -4702,7 +4702,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| ... | @@ -1403,7 +1473,38 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658 | ... | @@ -1403,7 +1473,38 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658 |
| 1403 | } | 1473 | } |
| 1404 | return nextSize; | 1474 | return nextSize; |
| 1405 | } | 1475 | } |
| 1406 | @@ -6446,8 +6453,8 @@ function ScrollAdjust() { | 1476 | @@ -5651,6 +5658,18 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) { |
| 1477 | }); | ||
| 1478 | |||
| 1479 | // src/utils/reordering.ts | ||
| 1480 | +// Element.moveBefore keeps the moved subtree's state and does not count as an | ||
| 1481 | +// insertion, so CSS @starting-style transitions and iframes inside a row do not | ||
| 1482 | +// restart when the list reorders its containers. insertBefore is the fallback. | ||
| 1483 | +function moveChildBefore(container, element, reference) { | ||
| 1484 | + if (typeof container.moveBefore === "function") { | ||
| 1485 | + container.moveBefore(element, reference); | ||
| 1486 | + } else if (reference) { | ||
| 1487 | + container.insertBefore(element, reference); | ||
| 1488 | + } else { | ||
| 1489 | + container.appendChild(element); | ||
| 1490 | + } | ||
| 1491 | +} | ||
| 1492 | function sortDOMElements(container, indexByElement) { | ||
| 1493 | const elements = Array.from(container.children); | ||
| 1494 | if (elements.length <= 1) return elements; | ||
| 1495 | @@ -5690,9 +5709,9 @@ function sortDOMElements(container, indexByElement) { | ||
| 1496 | } | ||
| 1497 | } | ||
| 1498 | if (nextStableElement) { | ||
| 1499 | - container.insertBefore(element, nextStableElement); | ||
| 1500 | + moveChildBefore(container, element, nextStableElement); | ||
| 1501 | } else { | ||
| 1502 | - container.appendChild(element); | ||
| 1503 | + moveChildBefore(container, element, null); | ||
| 1504 | } | ||
| 1505 | } | ||
| 1506 | } | ||
| 1507 | @@ -6446,8 +6465,8 @@ function ScrollAdjust() { | ||
| 1407 | window.getComputedStyle(contentNode)[axis.paddingEndProp] | 1508 | window.getComputedStyle(contentNode)[axis.paddingEndProp] |
| 1408 | ); | 1509 | ); |
| 1409 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; | 1510 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; |
| ... | @@ -1414,7 +1515,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658 | ... | @@ -1414,7 +1515,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658 |
| 1414 | scrollBy(); | 1515 | scrollBy(); |
| 1415 | if (resetPaddingRafRef.current !== void 0) { | 1516 | if (resetPaddingRafRef.current !== void 0) { |
| 1416 | diff --git a/react.mjs b/react.mjs | 1517 | diff --git a/react.mjs b/react.mjs |
| 1417 | index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b865331b4f 100644 | 1518 | index 95465f2..b73bfdf 100644 |
| 1418 | --- a/react.mjs | 1519 | --- a/react.mjs |
| 1419 | +++ b/react.mjs | 1520 | +++ b/react.mjs |
| 1420 | @@ -4681,7 +4681,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1521 | @@ -4681,7 +4681,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| ... | @@ -1440,7 +1541,38 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b8 | ... | @@ -1440,7 +1541,38 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b8 |
| 1440 | } | 1541 | } |
| 1441 | return nextSize; | 1542 | return nextSize; |
| 1442 | } | 1543 | } |
| 1443 | @@ -6425,8 +6432,8 @@ function ScrollAdjust() { | 1544 | @@ -5630,6 +5637,18 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) { |
| 1545 | }); | ||
| 1546 | |||
| 1547 | // src/utils/reordering.ts | ||
| 1548 | +// Element.moveBefore keeps the moved subtree's state and does not count as an | ||
| 1549 | +// insertion, so CSS @starting-style transitions and iframes inside a row do not | ||
| 1550 | +// restart when the list reorders its containers. insertBefore is the fallback. | ||
| 1551 | +function moveChildBefore(container, element, reference) { | ||
| 1552 | + if (typeof container.moveBefore === "function") { | ||
| 1553 | + container.moveBefore(element, reference); | ||
| 1554 | + } else if (reference) { | ||
| 1555 | + container.insertBefore(element, reference); | ||
| 1556 | + } else { | ||
| 1557 | + container.appendChild(element); | ||
| 1558 | + } | ||
| 1559 | +} | ||
| 1560 | function sortDOMElements(container, indexByElement) { | ||
| 1561 | const elements = Array.from(container.children); | ||
| 1562 | if (elements.length <= 1) return elements; | ||
| 1563 | @@ -5669,9 +5688,9 @@ function sortDOMElements(container, indexByElement) { | ||
| 1564 | } | ||
| 1565 | } | ||
| 1566 | if (nextStableElement) { | ||
| 1567 | - container.insertBefore(element, nextStableElement); | ||
| 1568 | + moveChildBefore(container, element, nextStableElement); | ||
| 1569 | } else { | ||
| 1570 | - container.appendChild(element); | ||
| 1571 | + moveChildBefore(container, element, null); | ||
| 1572 | } | ||
| 1573 | } | ||
| 1574 | } | ||
| 1575 | @@ -6425,8 +6444,8 @@ function ScrollAdjust() { | ||
| 1444 | window.getComputedStyle(contentNode)[axis.paddingEndProp] | 1576 | window.getComputedStyle(contentNode)[axis.paddingEndProp] |
| 1445 | ); | 1577 | ); |
| 1446 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; | 1578 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; |
| ... | @@ -1451,7 +1583,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b8 | ... | @@ -1451,7 +1583,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b8 |
| 1451 | scrollBy(); | 1583 | scrollBy(); |
| 1452 | if (resetPaddingRafRef.current !== void 0) { | 1584 | if (resetPaddingRafRef.current !== void 0) { |
| 1453 | diff --git a/reanimated.d.ts b/reanimated.d.ts | 1585 | diff --git a/reanimated.d.ts b/reanimated.d.ts |
| 1454 | index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613dae950fe 100644 | 1586 | index e504332..2ce6383 100644 |
| 1455 | --- a/reanimated.d.ts | 1587 | --- a/reanimated.d.ts |
| 1456 | +++ b/reanimated.d.ts | 1588 | +++ b/reanimated.d.ts |
| 1457 | @@ -294,6 +294,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | 1589 | @@ -294,6 +294,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { |
| ... | @@ -1468,7 +1600,7 @@ index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613 | ... | @@ -1468,7 +1600,7 @@ index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613 |
| 1468 | * Number of columns to render items in. | 1600 | * Number of columns to render items in. |
| 1469 | * @default 1 | 1601 | * @default 1 |
| 1470 | diff --git a/reanimated.js b/reanimated.js | 1602 | diff --git a/reanimated.js b/reanimated.js |
| 1471 | index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..fc03be2190c046f743ffde45ed189ee7868d0cec 100644 | 1603 | index f1265fa..fc03be2 100644 |
| 1472 | --- a/reanimated.js | 1604 | --- a/reanimated.js |
| 1473 | +++ b/reanimated.js | 1605 | +++ b/reanimated.js |
| 1474 | @@ -115,8 +115,10 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | 1606 | @@ -115,8 +115,10 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( |
| ... | @@ -1631,7 +1763,7 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..fc03be2190c046f743ffde45ed189ee7 | ... | @@ -1631,7 +1763,7 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..fc03be2190c046f743ffde45ed189ee7 |
| 1631 | renderScrollComponent: renderReanimatedScrollComponent, | 1763 | renderScrollComponent: renderReanimatedScrollComponent, |
| 1632 | ...IsNewArchitecture ? { stickyPositionComponentInternal } : {} | 1764 | ...IsNewArchitecture ? { stickyPositionComponentInternal } : {} |
| 1633 | diff --git a/reanimated.mjs b/reanimated.mjs | 1765 | diff --git a/reanimated.mjs b/reanimated.mjs |
| 1634 | index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..6ab929250c8924e2d9919ee09a4f774238fbb90a 100644 | 1766 | index 29a00d5..6ab9292 100644 |
| 1635 | --- a/reanimated.mjs | 1767 | --- a/reanimated.mjs |
| 1636 | +++ b/reanimated.mjs | 1768 | +++ b/reanimated.mjs |
| 1637 | @@ -1,7 +1,7 @@ | 1769 | @@ -1,7 +1,7 @@ |
pnpm-lock.yaml+5-5| ... | @@ -91,7 +91,7 @@ patchedDependencies: | ... | @@ -91,7 +91,7 @@ patchedDependencies: |
| 91 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b | 91 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b |
| 92 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 | 92 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 |
| 93 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 | 93 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 |
| 94 | '@legendapp/list@3.3.5': a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b | 94 | '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 |
| 95 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 | 95 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 |
| 96 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d | 96 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d |
| 97 | '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd | 97 | '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd |
| ... | @@ -248,7 +248,7 @@ importers: | ... | @@ -248,7 +248,7 @@ importers: |
| 248 | version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | 248 | version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| 249 | '@legendapp/list': | 249 | '@legendapp/list': |
| 250 | specifier: 'catalog:' | 250 | specifier: 'catalog:' |
| 251 | version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | 251 | version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| 252 | '@material/material-color-utilities': | 252 | '@material/material-color-utilities': |
| 253 | specifier: 0.3.0 | 253 | specifier: 0.3.0 |
| 254 | version: 0.3.0 | 254 | version: 0.3.0 |
| ... | @@ -607,7 +607,7 @@ importers: | ... | @@ -607,7 +607,7 @@ importers: |
| 607 | version: 0.9.0 | 607 | version: 0.9.0 |
| 608 | '@legendapp/list': | 608 | '@legendapp/list': |
| 609 | specifier: 'catalog:' | 609 | specifier: 'catalog:' |
| 610 | version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) | 610 | version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) |
| 611 | '@lexical/react': | 611 | '@lexical/react': |
| 612 | specifier: ^0.41.0 | 612 | specifier: ^0.41.0 |
| 613 | version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) | 613 | version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) |
| ... | @@ -13997,7 +13997,7 @@ snapshots: | ... | @@ -13997,7 +13997,7 @@ snapshots: |
| 13997 | dependencies: | 13997 | dependencies: |
| 13998 | jsbi: 4.3.2 | 13998 | jsbi: 4.3.2 |
| 13999 | 13999 | ||
| 14000 | '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': | 14000 | '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': |
| 14001 | dependencies: | 14001 | dependencies: |
| 14002 | react: 19.2.3 | 14002 | react: 19.2.3 |
| 14003 | use-sync-external-store: 1.6.0(react@19.2.3) | 14003 | use-sync-external-store: 1.6.0(react@19.2.3) |
| ... | @@ -14005,7 +14005,7 @@ snapshots: | ... | @@ -14005,7 +14005,7 @@ snapshots: |
| 14005 | react-dom: 19.2.3(react@19.2.3) | 14005 | react-dom: 19.2.3(react@19.2.3) |
| 14006 | react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) | 14006 | react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| 14007 | 14007 | ||
| 14008 | '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': | 14008 | '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': |
| 14009 | dependencies: | 14009 | dependencies: |
| 14010 | react: 19.2.6 | 14010 | react: 19.2.6 |
| 14011 | use-sync-external-store: 1.6.0(react@19.2.6) | 14011 | use-sync-external-store: 1.6.0(react@19.2.6) |