| ... | @@ -3,6 +3,7 @@ import type { | ... | @@ -3,6 +3,7 @@ import type { |
| 3 | MessageId, | 3 | MessageId, |
| 4 | OrchestrationCheckpointSummary, | 4 | OrchestrationCheckpointSummary, |
| 5 | OrchestrationEvent, | 5 | OrchestrationEvent, |
| | 6 | OrchestrationLatestTurn, |
| 6 | OrchestrationMessage, | 7 | OrchestrationMessage, |
| 7 | OrchestrationProposedPlan, | 8 | OrchestrationProposedPlan, |
| 8 | OrchestrationReadModel, | 9 | OrchestrationReadModel, |
| ... | @@ -22,12 +23,6 @@ import type { | ... | @@ -22,12 +23,6 @@ import type { |
| 22 | } from "@t3tools/contracts"; | 23 | } from "@t3tools/contracts"; |
| 23 | import { resolveModelSlugForProvider } from "@t3tools/shared/model"; | 24 | import { resolveModelSlugForProvider } from "@t3tools/shared/model"; |
| 24 | import { create } from "zustand"; | 25 | import { create } from "zustand"; |
| 25 | import { | | |
| 26 | derivePendingApprovals, | | |
| 27 | derivePendingUserInputs, | | |
| 28 | findLatestProposedPlan, | | |
| 29 | hasActionableProposedPlan, | | |
| 30 | } from "./session-logic"; | | |
| 31 | import { | 26 | import { |
| 32 | type ChatMessage, | 27 | type ChatMessage, |
| 33 | type Project, | 28 | type Project, |
| ... | @@ -46,11 +41,33 @@ import { getThreadFromEnvironmentState } from "./threadDerivation"; | ... | @@ -46,11 +41,33 @@ import { getThreadFromEnvironmentState } from "./threadDerivation"; |
| 46 | export interface EnvironmentState { | 41 | export interface EnvironmentState { |
| 47 | projectIds: ProjectId[]; | 42 | projectIds: ProjectId[]; |
| 48 | projectById: Record<ProjectId, Project>; | 43 | projectById: Record<ProjectId, Project>; |
| | 44 | |
| | 45 | // --------------------------------------------------------------------------- |
| | 46 | // Thread bookkeeping — written by BOTH shell stream and detail stream. |
| | 47 | // Both streams ensure the thread is registered here; the bookkeeping is |
| | 48 | // additive (append-only IDs) so concurrent writes are safe. |
| | 49 | // --------------------------------------------------------------------------- |
| 49 | threadIds: ThreadId[]; | 50 | threadIds: ThreadId[]; |
| 50 | threadIdsByProjectId: Record<ProjectId, ThreadId[]>; | 51 | threadIdsByProjectId: Record<ProjectId, ThreadId[]>; |
| | 52 | |
| | 53 | // --------------------------------------------------------------------------- |
| | 54 | // Thread shell / session / turn — written by BOTH shell stream and detail |
| | 55 | // stream. The shell stream is the *authoritative* source (server pre- |
| | 56 | // computes these from the projection pipeline), but the detail stream also |
| | 57 | // writes them so the active thread has up-to-date state even if the shell |
| | 58 | // event hasn't arrived yet. Structural equality checks in both write |
| | 59 | // functions prevent unnecessary React re-renders when both streams deliver |
| | 60 | // equivalent data. |
| | 61 | // --------------------------------------------------------------------------- |
| 51 | threadShellById: Record<ThreadId, ThreadShell>; | 62 | threadShellById: Record<ThreadId, ThreadShell>; |
| 52 | threadSessionById: Record<ThreadId, ThreadSession | null>; | 63 | threadSessionById: Record<ThreadId, ThreadSession | null>; |
| 53 | threadTurnStateById: Record<ThreadId, ThreadTurnState>; | 64 | threadTurnStateById: Record<ThreadId, ThreadTurnState>; |
| | 65 | |
| | 66 | // --------------------------------------------------------------------------- |
| | 67 | // Thread detail content — written ONLY by the detail stream |
| | 68 | // (writeThreadState / syncServerThreadDetail). The shell stream never |
| | 69 | // touches these. |
| | 70 | // --------------------------------------------------------------------------- |
| 54 | messageIdsByThreadId: Record<ThreadId, MessageId[]>; | 71 | messageIdsByThreadId: Record<ThreadId, MessageId[]>; |
| 55 | messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>>; | 72 | messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>>; |
| 56 | activityIdsByThreadId: Record<ThreadId, string[]>; | 73 | activityIdsByThreadId: Record<ThreadId, string[]>; |
| ... | @@ -59,7 +76,16 @@ export interface EnvironmentState { | ... | @@ -59,7 +76,16 @@ export interface EnvironmentState { |
| 59 | proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>>; | 76 | proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>>; |
| 60 | turnDiffIdsByThreadId: Record<ThreadId, TurnId[]>; | 77 | turnDiffIdsByThreadId: Record<ThreadId, TurnId[]>; |
| 61 | turnDiffSummaryByThreadId: Record<ThreadId, Record<TurnId, TurnDiffSummary>>; | 78 | turnDiffSummaryByThreadId: Record<ThreadId, Record<TurnId, TurnDiffSummary>>; |
| | 79 | |
| | 80 | // --------------------------------------------------------------------------- |
| | 81 | // Sidebar summary — written ONLY by the shell stream |
| | 82 | // (writeThreadShellState / mapThreadShell). Pre-computed server-side with |
| | 83 | // fields like latestUserMessageAt, hasPendingApprovals, etc. The detail |
| | 84 | // stream must NOT write here; the shell stream is the single source of |
| | 85 | // truth for sidebar data. |
| | 86 | // --------------------------------------------------------------------------- |
| 62 | sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary>; | 87 | sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary>; |
| | 88 | |
| 63 | bootstrapComplete: boolean; | 89 | bootstrapComplete: boolean; |
| 64 | } | 90 | } |
| 65 | | 91 | |
| ... | @@ -308,40 +334,47 @@ function toThreadTurnState(thread: Thread): ThreadTurnState { | ... | @@ -308,40 +334,47 @@ function toThreadTurnState(thread: Thread): ThreadTurnState { |
| 308 | }; | 334 | }; |
| 309 | } | 335 | } |
| 310 | | 336 | |
| 311 | function getLatestUserMessageAt(messages: ReadonlyArray<ChatMessage>): string | null { | 337 | function sourceProposedPlansEqual( |
| 312 | let latestUserMessageAt: string | null = null; | 338 | left: OrchestrationLatestTurn["sourceProposedPlan"] | undefined, |
| 313 | for (const message of messages) { | 339 | right: OrchestrationLatestTurn["sourceProposedPlan"] | undefined, |
| 314 | if (message.role !== "user") { | 340 | ): boolean { |
| 315 | continue; | 341 | if (left === right) return true; |
| 316 | } | 342 | if (left === undefined || right === undefined) return false; |
| 317 | if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { | 343 | return left.threadId === right.threadId && left.planId === right.planId; |
| 318 | latestUserMessageAt = message.createdAt; | | |
| 319 | } | | |
| 320 | } | | |
| 321 | return latestUserMessageAt; | | |
| 322 | } | 344 | } |
| 323 | | 345 | |
| 324 | function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { | 346 | function latestTurnsEqual( |
| 325 | return { | 347 | left: OrchestrationLatestTurn | null | undefined, |
| 326 | id: thread.id, | 348 | right: OrchestrationLatestTurn | null | undefined, |
| 327 | environmentId: thread.environmentId, | 349 | ): boolean { |
| 328 | projectId: thread.projectId, | 350 | if (left === right) return true; |
| 329 | title: thread.title, | 351 | if (left == null || right == null) return false; |
| 330 | interactionMode: thread.interactionMode, | 352 | return ( |
| 331 | session: thread.session, | 353 | left.turnId === right.turnId && |
| 332 | createdAt: thread.createdAt, | 354 | left.state === right.state && |
| 333 | archivedAt: thread.archivedAt, | 355 | left.requestedAt === right.requestedAt && |
| 334 | updatedAt: thread.updatedAt, | 356 | left.startedAt === right.startedAt && |
| 335 | latestTurn: thread.latestTurn, | 357 | left.completedAt === right.completedAt && |
| 336 | branch: thread.branch, | 358 | left.assistantMessageId === right.assistantMessageId && |
| 337 | worktreePath: thread.worktreePath, | 359 | sourceProposedPlansEqual(left.sourceProposedPlan, right.sourceProposedPlan) |
| 338 | latestUserMessageAt: getLatestUserMessageAt(thread.messages), | 360 | ); |
| 339 | hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, | 361 | } |
| 340 | hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, | 362 | |
| 341 | hasActionableProposedPlan: hasActionableProposedPlan( | 363 | function threadSessionsEqual( |
| 342 | findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), | 364 | left: ThreadSession | null | undefined, |
| 343 | ), | 365 | right: ThreadSession | null | undefined, |
| 344 | }; | 366 | ): boolean { |
| | 367 | if (left === right) return true; |
| | 368 | if (left == null || right == null) return false; |
| | 369 | return ( |
| | 370 | left.provider === right.provider && |
| | 371 | left.status === right.status && |
| | 372 | left.orchestrationStatus === right.orchestrationStatus && |
| | 373 | left.activeTurnId === right.activeTurnId && |
| | 374 | left.createdAt === right.createdAt && |
| | 375 | left.updatedAt === right.updatedAt && |
| | 376 | left.lastError === right.lastError |
| | 377 | ); |
| 345 | } | 378 | } |
| 346 | | 379 | |
| 347 | function sidebarThreadSummariesEqual( | 380 | function sidebarThreadSummariesEqual( |
| ... | @@ -354,11 +387,11 @@ function sidebarThreadSummariesEqual( | ... | @@ -354,11 +387,11 @@ function sidebarThreadSummariesEqual( |
| 354 | left.projectId === right.projectId && | 387 | left.projectId === right.projectId && |
| 355 | left.title === right.title && | 388 | left.title === right.title && |
| 356 | left.interactionMode === right.interactionMode && | 389 | left.interactionMode === right.interactionMode && |
| 357 | left.session === right.session && | 390 | threadSessionsEqual(left.session, right.session) && |
| 358 | left.createdAt === right.createdAt && | 391 | left.createdAt === right.createdAt && |
| 359 | left.archivedAt === right.archivedAt && | 392 | left.archivedAt === right.archivedAt && |
| 360 | left.updatedAt === right.updatedAt && | 393 | left.updatedAt === right.updatedAt && |
| 361 | left.latestTurn === right.latestTurn && | 394 | latestTurnsEqual(left.latestTurn, right.latestTurn) && |
| 362 | left.branch === right.branch && | 395 | left.branch === right.branch && |
| 363 | left.worktreePath === right.worktreePath && | 396 | left.worktreePath === right.worktreePath && |
| 364 | left.latestUserMessageAt === right.latestUserMessageAt && | 397 | left.latestUserMessageAt === right.latestUserMessageAt && |
| ... | @@ -391,8 +424,8 @@ function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): b | ... | @@ -391,8 +424,8 @@ function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): b |
| 391 | function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean { | 424 | function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean { |
| 392 | return ( | 425 | return ( |
| 393 | left !== undefined && | 426 | left !== undefined && |
| 394 | left.latestTurn === right.latestTurn && | 427 | latestTurnsEqual(left.latestTurn, right.latestTurn) && |
| 395 | left.pendingSourceProposedPlan === right.pendingSourceProposedPlan | 428 | sourceProposedPlansEqual(left.pendingSourceProposedPlan, right.pendingSourceProposedPlan) |
| 396 | ); | 429 | ); |
| 397 | } | 430 | } |
| 398 | | 431 | |
| ... | @@ -466,34 +499,32 @@ function getThreads(state: EnvironmentState): Thread[] { | ... | @@ -466,34 +499,32 @@ function getThreads(state: EnvironmentState): Thread[] { |
| 466 | }); | 499 | }); |
| 467 | } | 500 | } |
| 468 | | 501 | |
| 469 | function writeThreadState( | 502 | /** |
| | 503 | * Ensure a thread is registered in the bookkeeping indices (threadIds, |
| | 504 | * threadIdsByProjectId). Shared by both the shell stream and detail stream |
| | 505 | * write paths — the bookkeeping is additive (append-only IDs) so concurrent |
| | 506 | * writes from both streams are safe. |
| | 507 | */ |
| | 508 | function ensureThreadRegistered( |
| 470 | state: EnvironmentState, | 509 | state: EnvironmentState, |
| 471 | nextThread: Thread, | 510 | threadId: ThreadId, |
| 472 | previousThread?: Thread, | 511 | nextProjectId: ProjectId, |
| | 512 | previousProjectId: ProjectId | undefined, |
| 473 | ): EnvironmentState { | 513 | ): EnvironmentState { |
| 474 | const nextShell = toThreadShell(nextThread); | | |
| 475 | const nextTurnState = toThreadTurnState(nextThread); | | |
| 476 | const previousShell = state.threadShellById[nextThread.id]; | | |
| 477 | const previousTurnState = state.threadTurnStateById[nextThread.id]; | | |
| 478 | const previousSummary = state.sidebarThreadSummaryById[nextThread.id]; | | |
| 479 | const nextSummary = buildSidebarThreadSummary(nextThread); | | |
| 480 | | | |
| 481 | let nextState = state; | 514 | let nextState = state; |
| 482 | | 515 | |
| 483 | if (!state.threadIds.includes(nextThread.id)) { | 516 | if (!state.threadIds.includes(threadId)) { |
| 484 | nextState = { | 517 | nextState = { |
| 485 | ...nextState, | 518 | ...nextState, |
| 486 | threadIds: [...nextState.threadIds, nextThread.id], | 519 | threadIds: [...nextState.threadIds, threadId], |
| 487 | }; | 520 | }; |
| 488 | } | 521 | } |
| 489 | | 522 | |
| 490 | const previousProjectId = previousThread?.projectId; | | |
| 491 | const nextProjectId = nextThread.projectId; | | |
| 492 | if (previousProjectId !== nextProjectId) { | 523 | if (previousProjectId !== nextProjectId) { |
| 493 | let threadIdsByProjectId = nextState.threadIdsByProjectId; | 524 | let threadIdsByProjectId = nextState.threadIdsByProjectId; |
| 494 | if (previousProjectId) { | 525 | if (previousProjectId) { |
| 495 | const previousIds = threadIdsByProjectId[previousProjectId] ?? EMPTY_THREAD_IDS; | 526 | const previousIds = threadIdsByProjectId[previousProjectId] ?? EMPTY_THREAD_IDS; |
| 496 | const nextIds = removeId(previousIds, nextThread.id); | 527 | const nextIds = removeId(previousIds, threadId); |
| 497 | if (nextIds.length === 0) { | 528 | if (nextIds.length === 0) { |
| 498 | const { [previousProjectId]: _removed, ...rest } = threadIdsByProjectId; | 529 | const { [previousProjectId]: _removed, ...rest } = threadIdsByProjectId; |
| 499 | threadIdsByProjectId = rest as Record<ProjectId, ThreadId[]>; | 530 | threadIdsByProjectId = rest as Record<ProjectId, ThreadId[]>; |
| ... | @@ -505,7 +536,7 @@ function writeThreadState( | ... | @@ -505,7 +536,7 @@ function writeThreadState( |
| 505 | } | 536 | } |
| 506 | } | 537 | } |
| 507 | const projectThreadIds = threadIdsByProjectId[nextProjectId] ?? EMPTY_THREAD_IDS; | 538 | const projectThreadIds = threadIdsByProjectId[nextProjectId] ?? EMPTY_THREAD_IDS; |
| 508 | const nextProjectThreadIds = appendId(projectThreadIds, nextThread.id); | 539 | const nextProjectThreadIds = appendId(projectThreadIds, threadId); |
| 509 | if (!arraysEqual(projectThreadIds, nextProjectThreadIds)) { | 540 | if (!arraysEqual(projectThreadIds, nextProjectThreadIds)) { |
| 510 | threadIdsByProjectId = { | 541 | threadIdsByProjectId = { |
| 511 | ...threadIdsByProjectId, | 542 | ...threadIdsByProjectId, |
| ... | @@ -520,6 +551,36 @@ function writeThreadState( | ... | @@ -520,6 +551,36 @@ function writeThreadState( |
| 520 | } | 551 | } |
| 521 | } | 552 | } |
| 522 | | 553 | |
| | 554 | return nextState; |
| | 555 | } |
| | 556 | |
| | 557 | /** |
| | 558 | * Write thread state from the **detail stream** (per-thread subscription). |
| | 559 | * |
| | 560 | * Owns: messages, activities, proposed plans, turn diff summaries. |
| | 561 | * Also writes threadShellById / threadSessionById / threadTurnStateById so |
| | 562 | * the active thread has up-to-date state even if the shell stream event |
| | 563 | * hasn't arrived yet (both streams use structural equality checks to avoid |
| | 564 | * unnecessary re-renders when delivering equivalent data). |
| | 565 | * Does NOT write sidebarThreadSummaryById — that is shell-stream-only. |
| | 566 | */ |
| | 567 | function writeThreadState( |
| | 568 | state: EnvironmentState, |
| | 569 | nextThread: Thread, |
| | 570 | previousThread?: Thread, |
| | 571 | ): EnvironmentState { |
| | 572 | const nextShell = toThreadShell(nextThread); |
| | 573 | const nextTurnState = toThreadTurnState(nextThread); |
| | 574 | const previousShell = state.threadShellById[nextThread.id]; |
| | 575 | const previousTurnState = state.threadTurnStateById[nextThread.id]; |
| | 576 | |
| | 577 | let nextState = ensureThreadRegistered( |
| | 578 | state, |
| | 579 | nextThread.id, |
| | 580 | nextThread.projectId, |
| | 581 | previousThread?.projectId, |
| | 582 | ); |
| | 583 | |
| 523 | if (!threadShellsEqual(previousShell, nextShell)) { | 584 | if (!threadShellsEqual(previousShell, nextShell)) { |
| 524 | nextState = { | 585 | nextState = { |
| 525 | ...nextState, | 586 | ...nextState, |
| ... | @@ -530,7 +591,7 @@ function writeThreadState( | ... | @@ -530,7 +591,7 @@ function writeThreadState( |
| 530 | }; | 591 | }; |
| 531 | } | 592 | } |
| 532 | | 593 | |
| 533 | if ((previousThread?.session ?? null) !== nextThread.session) { | 594 | if (!threadSessionsEqual(previousThread?.session ?? null, nextThread.session)) { |
| 534 | nextState = { | 595 | nextState = { |
| 535 | ...nextState, | 596 | ...nextState, |
| 536 | threadSessionById: { | 597 | threadSessionById: { |
| ... | @@ -610,19 +671,20 @@ function writeThreadState( | ... | @@ -610,19 +671,20 @@ function writeThreadState( |
| 610 | }; | 671 | }; |
| 611 | } | 672 | } |
| 612 | | 673 | |
| 613 | if (!sidebarThreadSummariesEqual(previousSummary, nextSummary)) { | | |
| 614 | nextState = { | | |
| 615 | ...nextState, | | |
| 616 | sidebarThreadSummaryById: { | | |
| 617 | ...nextState.sidebarThreadSummaryById, | | |
| 618 | [nextThread.id]: nextSummary, | | |
| 619 | }, | | |
| 620 | }; | | |
| 621 | } | | |
| 622 | | | |
| 623 | return nextState; | 674 | return nextState; |
| 624 | } | 675 | } |
| 625 | | 676 | |
| | 677 | /** |
| | 678 | * Write thread state from the **shell stream** (all-threads subscription). |
| | 679 | * |
| | 680 | * Owns: sidebarThreadSummaryById (pre-computed server-side sidebar data). |
| | 681 | * Also writes threadShellById / threadSessionById / threadTurnStateById as |
| | 682 | * the authoritative source for these fields. The detail stream may also |
| | 683 | * write them for the focused thread (see writeThreadState); structural |
| | 684 | * equality checks prevent unnecessary re-renders. |
| | 685 | * Does NOT write message/activity/proposedPlan/turnDiff content — that is |
| | 686 | * detail-stream-only. |
| | 687 | */ |
| 626 | function writeThreadShellState( | 688 | function writeThreadShellState( |
| 627 | state: EnvironmentState, | 689 | state: EnvironmentState, |
| 628 | nextThread: { | 690 | nextThread: { |
| ... | @@ -632,49 +694,14 @@ function writeThreadShellState( | ... | @@ -632,49 +694,14 @@ function writeThreadShellState( |
| 632 | summary: SidebarThreadSummary; | 694 | summary: SidebarThreadSummary; |
| 633 | }, | 695 | }, |
| 634 | ): EnvironmentState { | 696 | ): EnvironmentState { |
| 635 | let nextState = state; | | |
| 636 | const previousShell = state.threadShellById[nextThread.shell.id]; | 697 | const previousShell = state.threadShellById[nextThread.shell.id]; |
| 637 | const previousProjectId = previousShell?.projectId; | | |
| 638 | const nextProjectId = nextThread.shell.projectId; | | |
| 639 | | 698 | |
| 640 | if (!state.threadIds.includes(nextThread.shell.id)) { | 699 | let nextState = ensureThreadRegistered( |
| 641 | nextState = { | 700 | state, |
| 642 | ...nextState, | 701 | nextThread.shell.id, |
| 643 | threadIds: [...nextState.threadIds, nextThread.shell.id], | 702 | nextThread.shell.projectId, |
| 644 | }; | 703 | previousShell?.projectId, |
| 645 | } | 704 | ); |
| 646 | | | |
| 647 | if (previousProjectId !== nextProjectId) { | | |
| 648 | let threadIdsByProjectId = nextState.threadIdsByProjectId; | | |
| 649 | if (previousProjectId) { | | |
| 650 | const previousIds = threadIdsByProjectId[previousProjectId] ?? EMPTY_THREAD_IDS; | | |
| 651 | const nextIds = removeId(previousIds, nextThread.shell.id); | | |
| 652 | if (nextIds.length === 0) { | | |
| 653 | const { [previousProjectId]: _removed, ...rest } = threadIdsByProjectId; | | |
| 654 | threadIdsByProjectId = rest as Record<ProjectId, ThreadId[]>; | | |
| 655 | } else if (!arraysEqual(previousIds, nextIds)) { | | |
| 656 | threadIdsByProjectId = { | | |
| 657 | ...threadIdsByProjectId, | | |
| 658 | [previousProjectId]: nextIds, | | |
| 659 | }; | | |
| 660 | } | | |
| 661 | } | | |
| 662 | | | |
| 663 | const projectThreadIds = threadIdsByProjectId[nextProjectId] ?? EMPTY_THREAD_IDS; | | |
| 664 | const nextProjectThreadIds = appendId(projectThreadIds, nextThread.shell.id); | | |
| 665 | if (!arraysEqual(projectThreadIds, nextProjectThreadIds)) { | | |
| 666 | threadIdsByProjectId = { | | |
| 667 | ...threadIdsByProjectId, | | |
| 668 | [nextProjectId]: nextProjectThreadIds, | | |
| 669 | }; | | |
| 670 | } | | |
| 671 | if (threadIdsByProjectId !== nextState.threadIdsByProjectId) { | | |
| 672 | nextState = { | | |
| 673 | ...nextState, | | |
| 674 | threadIdsByProjectId, | | |
| 675 | }; | | |
| 676 | } | | |
| 677 | } | | |
| 678 | | 705 | |
| 679 | if (!threadShellsEqual(previousShell, nextThread.shell)) { | 706 | if (!threadShellsEqual(previousShell, nextThread.shell)) { |
| 680 | nextState = { | 707 | nextState = { |
| ... | @@ -686,7 +713,9 @@ function writeThreadShellState( | ... | @@ -686,7 +713,9 @@ function writeThreadShellState( |
| 686 | }; | 713 | }; |
| 687 | } | 714 | } |
| 688 | | 715 | |
| 689 | if ((state.threadSessionById[nextThread.shell.id] ?? null) !== nextThread.session) { | 716 | if ( |
| | 717 | !threadSessionsEqual(state.threadSessionById[nextThread.shell.id] ?? null, nextThread.session) |
| | 718 | ) { |
| 690 | nextState = { | 719 | nextState = { |
| 691 | ...nextState, | 720 | ...nextState, |
| 692 | threadSessionById: { | 721 | threadSessionById: { |
| ... | @@ -1009,82 +1038,6 @@ function buildProjectState( | ... | @@ -1009,82 +1038,6 @@ function buildProjectState( |
| 1009 | }; | 1038 | }; |
| 1010 | } | 1039 | } |
| 1011 | | 1040 | |
| 1012 | function buildThreadState( | | |
| 1013 | threads: ReadonlyArray<Thread>, | | |
| 1014 | ): Pick< | | |
| 1015 | EnvironmentState, | | |
| 1016 | | "threadIds" | | |
| 1017 | | "threadIdsByProjectId" | | |
| 1018 | | "threadShellById" | | |
| 1019 | | "threadSessionById" | | |
| 1020 | | "threadTurnStateById" | | |
| 1021 | | "messageIdsByThreadId" | | |
| 1022 | | "messageByThreadId" | | |
| 1023 | | "activityIdsByThreadId" | | |
| 1024 | | "activityByThreadId" | | |
| 1025 | | "proposedPlanIdsByThreadId" | | |
| 1026 | | "proposedPlanByThreadId" | | |
| 1027 | | "turnDiffIdsByThreadId" | | |
| 1028 | | "turnDiffSummaryByThreadId" | | |
| 1029 | | "sidebarThreadSummaryById" | | |
| 1030 | > { | | |
| 1031 | const threadIds: ThreadId[] = []; | | |
| 1032 | const threadIdsByProjectId: Record<ProjectId, ThreadId[]> = {}; | | |
| 1033 | const threadShellById: Record<ThreadId, ThreadShell> = {}; | | |
| 1034 | const threadSessionById: Record<ThreadId, ThreadSession | null> = {}; | | |
| 1035 | const threadTurnStateById: Record<ThreadId, ThreadTurnState> = {}; | | |
| 1036 | const messageIdsByThreadId: Record<ThreadId, MessageId[]> = {}; | | |
| 1037 | const messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>> = {}; | | |
| 1038 | const activityIdsByThreadId: Record<ThreadId, string[]> = {}; | | |
| 1039 | const activityByThreadId: Record<ThreadId, Record<string, OrchestrationThreadActivity>> = {}; | | |
| 1040 | const proposedPlanIdsByThreadId: Record<ThreadId, string[]> = {}; | | |
| 1041 | const proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>> = {}; | | |
| 1042 | const turnDiffIdsByThreadId: Record<ThreadId, TurnId[]> = {}; | | |
| 1043 | const turnDiffSummaryByThreadId: Record<ThreadId, Record<TurnId, TurnDiffSummary>> = {}; | | |
| 1044 | const sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary> = {}; | | |
| 1045 | | | |
| 1046 | for (const thread of threads) { | | |
| 1047 | threadIds.push(thread.id); | | |
| 1048 | threadIdsByProjectId[thread.projectId] = [ | | |
| 1049 | ...(threadIdsByProjectId[thread.projectId] ?? EMPTY_THREAD_IDS), | | |
| 1050 | thread.id, | | |
| 1051 | ]; | | |
| 1052 | threadShellById[thread.id] = toThreadShell(thread); | | |
| 1053 | threadSessionById[thread.id] = thread.session; | | |
| 1054 | threadTurnStateById[thread.id] = toThreadTurnState(thread); | | |
| 1055 | const messageSlice = buildMessageSlice(thread); | | |
| 1056 | messageIdsByThreadId[thread.id] = messageSlice.ids; | | |
| 1057 | messageByThreadId[thread.id] = messageSlice.byId; | | |
| 1058 | const activitySlice = buildActivitySlice(thread); | | |
| 1059 | activityIdsByThreadId[thread.id] = activitySlice.ids; | | |
| 1060 | activityByThreadId[thread.id] = activitySlice.byId; | | |
| 1061 | const proposedPlanSlice = buildProposedPlanSlice(thread); | | |
| 1062 | proposedPlanIdsByThreadId[thread.id] = proposedPlanSlice.ids; | | |
| 1063 | proposedPlanByThreadId[thread.id] = proposedPlanSlice.byId; | | |
| 1064 | const turnDiffSlice = buildTurnDiffSlice(thread); | | |
| 1065 | turnDiffIdsByThreadId[thread.id] = turnDiffSlice.ids; | | |
| 1066 | turnDiffSummaryByThreadId[thread.id] = turnDiffSlice.byId; | | |
| 1067 | sidebarThreadSummaryById[thread.id] = buildSidebarThreadSummary(thread); | | |
| 1068 | } | | |
| 1069 | | | |
| 1070 | return { | | |
| 1071 | threadIds, | | |
| 1072 | threadIdsByProjectId, | | |
| 1073 | threadShellById, | | |
| 1074 | threadSessionById, | | |
| 1075 | threadTurnStateById, | | |
| 1076 | messageIdsByThreadId, | | |
| 1077 | messageByThreadId, | | |
| 1078 | activityIdsByThreadId, | | |
| 1079 | activityByThreadId, | | |
| 1080 | proposedPlanIdsByThreadId, | | |
| 1081 | proposedPlanByThreadId, | | |
| 1082 | turnDiffIdsByThreadId, | | |
| 1083 | turnDiffSummaryByThreadId, | | |
| 1084 | sidebarThreadSummaryById, | | |
| 1085 | }; | | |
| 1086 | } | | |
| 1087 | | | |
| 1088 | function getStoredEnvironmentState( | 1041 | function getStoredEnvironmentState( |
| 1089 | state: AppState, | 1042 | state: AppState, |
| 1090 | environmentId: EnvironmentId, | 1043 | environmentId: EnvironmentId, |
| ... | @@ -1116,25 +1069,6 @@ function commitEnvironmentState( | ... | @@ -1116,25 +1069,6 @@ function commitEnvironmentState( |
| 1116 | }; | 1069 | }; |
| 1117 | } | 1070 | } |
| 1118 | | 1071 | |
| 1119 | function syncEnvironmentReadModel( | | |
| 1120 | state: EnvironmentState, | | |
| 1121 | readModel: OrchestrationReadModel, | | |
| 1122 | environmentId: EnvironmentId, | | |
| 1123 | ): EnvironmentState { | | |
| 1124 | const projects = readModel.projects | | |
| 1125 | .filter((project) => project.deletedAt === null) | | |
| 1126 | .map((project) => mapProject(project, environmentId)); | | |
| 1127 | const threads = readModel.threads | | |
| 1128 | .filter((thread) => thread.deletedAt === null) | | |
| 1129 | .map((thread) => mapThread(thread, environmentId)); | | |
| 1130 | return { | | |
| 1131 | ...state, | | |
| 1132 | ...buildProjectState(projects), | | |
| 1133 | ...buildThreadState(threads), | | |
| 1134 | bootstrapComplete: true, | | |
| 1135 | }; | | |
| 1136 | } | | |
| 1137 | | | |
| 1138 | function syncEnvironmentShellSnapshot( | 1072 | function syncEnvironmentShellSnapshot( |
| 1139 | state: EnvironmentState, | 1073 | state: EnvironmentState, |
| 1140 | snapshot: OrchestrationShellSnapshot, | 1074 | snapshot: OrchestrationShellSnapshot, |
| ... | @@ -1175,22 +1109,6 @@ function syncEnvironmentShellSnapshot( | ... | @@ -1175,22 +1109,6 @@ function syncEnvironmentShellSnapshot( |
| 1175 | return nextState; | 1109 | return nextState; |
| 1176 | } | 1110 | } |
| 1177 | | 1111 | |
| 1178 | export function syncServerReadModel( | | |
| 1179 | state: AppState, | | |
| 1180 | readModel: OrchestrationReadModel, | | |
| 1181 | environmentId: EnvironmentId, | | |
| 1182 | ): AppState { | | |
| 1183 | return commitEnvironmentState( | | |
| 1184 | state, | | |
| 1185 | environmentId, | | |
| 1186 | syncEnvironmentReadModel( | | |
| 1187 | getStoredEnvironmentState(state, environmentId), | | |
| 1188 | readModel, | | |
| 1189 | environmentId, | | |
| 1190 | ), | | |
| 1191 | ); | | |
| 1192 | } | | |
| 1193 | | | |
| 1194 | export function syncServerShellSnapshot( | 1112 | export function syncServerShellSnapshot( |
| 1195 | state: AppState, | 1113 | state: AppState, |
| 1196 | snapshot: OrchestrationShellSnapshot, | 1114 | snapshot: OrchestrationShellSnapshot, |
| ... | @@ -2007,7 +1925,6 @@ export function setThreadBranch( | ... | @@ -2007,7 +1925,6 @@ export function setThreadBranch( |
| 2007 | | 1925 | |
| 2008 | interface AppStore extends AppState { | 1926 | interface AppStore extends AppState { |
| 2009 | setActiveEnvironmentId: (environmentId: EnvironmentId) => void; | 1927 | setActiveEnvironmentId: (environmentId: EnvironmentId) => void; |
| 2010 | syncServerReadModel: (readModel: OrchestrationReadModel, environmentId: EnvironmentId) => void; | | |
| 2011 | syncServerShellSnapshot: ( | 1928 | syncServerShellSnapshot: ( |
| 2012 | snapshot: OrchestrationShellSnapshot, | 1929 | snapshot: OrchestrationShellSnapshot, |
| 2013 | environmentId: EnvironmentId, | 1930 | environmentId: EnvironmentId, |
| ... | @@ -2031,8 +1948,6 @@ export const useStore = create<AppStore>((set) => ({ | ... | @@ -2031,8 +1948,6 @@ export const useStore = create<AppStore>((set) => ({ |
| 2031 | ...initialState, | 1948 | ...initialState, |
| 2032 | setActiveEnvironmentId: (environmentId) => | 1949 | setActiveEnvironmentId: (environmentId) => |
| 2033 | set((state) => setActiveEnvironmentId(state, environmentId)), | 1950 | set((state) => setActiveEnvironmentId(state, environmentId)), |
| 2034 | syncServerReadModel: (readModel, environmentId) => | | |
| 2035 | set((state) => syncServerReadModel(state, readModel, environmentId)), | | |
| 2036 | syncServerShellSnapshot: (snapshot, environmentId) => | 1951 | syncServerShellSnapshot: (snapshot, environmentId) => |
| 2037 | set((state) => syncServerShellSnapshot(state, snapshot, environmentId)), | 1952 | set((state) => syncServerShellSnapshot(state, snapshot, environmentId)), |
| 2038 | syncServerThreadDetail: (thread, environmentId) => | 1953 | syncServerThreadDetail: (thread, environmentId) => |