| ... | ... | @@ -3,6 +3,7 @@ import type { |
| 3 | 3 | MessageId, |
| 4 | 4 | OrchestrationCheckpointSummary, |
| 5 | 5 | OrchestrationEvent, |
| 6 | OrchestrationLatestTurn, |
| 6 | 7 | OrchestrationMessage, |
| 7 | 8 | OrchestrationProposedPlan, |
| 8 | 9 | OrchestrationReadModel, |
| ... | ... | @@ -22,12 +23,6 @@ import type { |
| 22 | 23 | } from "@t3tools/contracts"; |
| 23 | 24 | import { resolveModelSlugForProvider } from "@t3tools/shared/model"; |
| 24 | 25 | import { create } from "zustand"; |
| 25 | | import { |
| 26 | | derivePendingApprovals, |
| 27 | | derivePendingUserInputs, |
| 28 | | findLatestProposedPlan, |
| 29 | | hasActionableProposedPlan, |
| 30 | | } from "./session-logic"; |
| 31 | 26 | import { |
| 32 | 27 | type ChatMessage, |
| 33 | 28 | type Project, |
| ... | ... | @@ -46,11 +41,33 @@ import { getThreadFromEnvironmentState } from "./threadDerivation"; |
| 46 | 41 | export interface EnvironmentState { |
| 47 | 42 | projectIds: ProjectId[]; |
| 48 | 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 | 50 | threadIds: ThreadId[]; |
| 50 | 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 | 62 | threadShellById: Record<ThreadId, ThreadShell>; |
| 52 | 63 | threadSessionById: Record<ThreadId, ThreadSession | null>; |
| 53 | 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 | 71 | messageIdsByThreadId: Record<ThreadId, MessageId[]>; |
| 55 | 72 | messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>>; |
| 56 | 73 | activityIdsByThreadId: Record<ThreadId, string[]>; |
| ... | ... | @@ -59,7 +76,16 @@ export interface EnvironmentState { |
| 59 | 76 | proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>>; |
| 60 | 77 | turnDiffIdsByThreadId: Record<ThreadId, TurnId[]>; |
| 61 | 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 | 87 | sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary>; |
| 88 | |
| 63 | 89 | bootstrapComplete: boolean; |
| 64 | 90 | } |
| 65 | 91 | |
| ... | ... | @@ -308,40 +334,47 @@ function toThreadTurnState(thread: Thread): ThreadTurnState { |
| 308 | 334 | }; |
| 309 | 335 | } |
| 310 | 336 | |
| 311 | | function getLatestUserMessageAt(messages: ReadonlyArray<ChatMessage>): string | null { |
| 312 | | let latestUserMessageAt: string | null = null; |
| 313 | | for (const message of messages) { |
| 314 | | if (message.role !== "user") { |
| 315 | | continue; |
| 316 | | } |
| 317 | | if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { |
| 318 | | latestUserMessageAt = message.createdAt; |
| 319 | | } |
| 320 | | } |
| 321 | | return latestUserMessageAt; |
| 337 | function sourceProposedPlansEqual( |
| 338 | left: OrchestrationLatestTurn["sourceProposedPlan"] | undefined, |
| 339 | right: OrchestrationLatestTurn["sourceProposedPlan"] | undefined, |
| 340 | ): boolean { |
| 341 | if (left === right) return true; |
| 342 | if (left === undefined || right === undefined) return false; |
| 343 | return left.threadId === right.threadId && left.planId === right.planId; |
| 322 | 344 | } |
| 323 | 345 | |
| 324 | | function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { |
| 325 | | return { |
| 326 | | id: thread.id, |
| 327 | | environmentId: thread.environmentId, |
| 328 | | projectId: thread.projectId, |
| 329 | | title: thread.title, |
| 330 | | interactionMode: thread.interactionMode, |
| 331 | | session: thread.session, |
| 332 | | createdAt: thread.createdAt, |
| 333 | | archivedAt: thread.archivedAt, |
| 334 | | updatedAt: thread.updatedAt, |
| 335 | | latestTurn: thread.latestTurn, |
| 336 | | branch: thread.branch, |
| 337 | | worktreePath: thread.worktreePath, |
| 338 | | latestUserMessageAt: getLatestUserMessageAt(thread.messages), |
| 339 | | hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, |
| 340 | | hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, |
| 341 | | hasActionableProposedPlan: hasActionableProposedPlan( |
| 342 | | findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), |
| 343 | | ), |
| 344 | | }; |
| 346 | function latestTurnsEqual( |
| 347 | left: OrchestrationLatestTurn | null | undefined, |
| 348 | right: OrchestrationLatestTurn | null | undefined, |
| 349 | ): boolean { |
| 350 | if (left === right) return true; |
| 351 | if (left == null || right == null) return false; |
| 352 | return ( |
| 353 | left.turnId === right.turnId && |
| 354 | left.state === right.state && |
| 355 | left.requestedAt === right.requestedAt && |
| 356 | left.startedAt === right.startedAt && |
| 357 | left.completedAt === right.completedAt && |
| 358 | left.assistantMessageId === right.assistantMessageId && |
| 359 | sourceProposedPlansEqual(left.sourceProposedPlan, right.sourceProposedPlan) |
| 360 | ); |
| 361 | } |
| 362 | |
| 363 | function threadSessionsEqual( |
| 364 | left: ThreadSession | null | undefined, |
| 365 | right: ThreadSession | null | undefined, |
| 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 | 380 | function sidebarThreadSummariesEqual( |
| ... | ... | @@ -354,11 +387,11 @@ function sidebarThreadSummariesEqual( |
| 354 | 387 | left.projectId === right.projectId && |
| 355 | 388 | left.title === right.title && |
| 356 | 389 | left.interactionMode === right.interactionMode && |
| 357 | | left.session === right.session && |
| 390 | threadSessionsEqual(left.session, right.session) && |
| 358 | 391 | left.createdAt === right.createdAt && |
| 359 | 392 | left.archivedAt === right.archivedAt && |
| 360 | 393 | left.updatedAt === right.updatedAt && |
| 361 | | left.latestTurn === right.latestTurn && |
| 394 | latestTurnsEqual(left.latestTurn, right.latestTurn) && |
| 362 | 395 | left.branch === right.branch && |
| 363 | 396 | left.worktreePath === right.worktreePath && |
| 364 | 397 | left.latestUserMessageAt === right.latestUserMessageAt && |
| ... | ... | @@ -391,8 +424,8 @@ function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): b |
| 391 | 424 | function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean { |
| 392 | 425 | return ( |
| 393 | 426 | left !== undefined && |
| 394 | | left.latestTurn === right.latestTurn && |
| 395 | | left.pendingSourceProposedPlan === right.pendingSourceProposedPlan |
| 427 | latestTurnsEqual(left.latestTurn, right.latestTurn) && |
| 428 | sourceProposedPlansEqual(left.pendingSourceProposedPlan, right.pendingSourceProposedPlan) |
| 396 | 429 | ); |
| 397 | 430 | } |
| 398 | 431 | |
| ... | ... | @@ -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 | 509 | state: EnvironmentState, |
| 471 | | nextThread: Thread, |
| 472 | | previousThread?: Thread, |
| 510 | threadId: ThreadId, |
| 511 | nextProjectId: ProjectId, |
| 512 | previousProjectId: ProjectId | undefined, |
| 473 | 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 | 514 | let nextState = state; |
| 482 | 515 | |
| 483 | | if (!state.threadIds.includes(nextThread.id)) { |
| 516 | if (!state.threadIds.includes(threadId)) { |
| 484 | 517 | nextState = { |
| 485 | 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 | 523 | if (previousProjectId !== nextProjectId) { |
| 493 | 524 | let threadIdsByProjectId = nextState.threadIdsByProjectId; |
| 494 | 525 | if (previousProjectId) { |
| 495 | 526 | const previousIds = threadIdsByProjectId[previousProjectId] ?? EMPTY_THREAD_IDS; |
| 496 | | const nextIds = removeId(previousIds, nextThread.id); |
| 527 | const nextIds = removeId(previousIds, threadId); |
| 497 | 528 | if (nextIds.length === 0) { |
| 498 | 529 | const { [previousProjectId]: _removed, ...rest } = threadIdsByProjectId; |
| 499 | 530 | threadIdsByProjectId = rest as Record<ProjectId, ThreadId[]>; |
| ... | ... | @@ -505,7 +536,7 @@ function writeThreadState( |
| 505 | 536 | } |
| 506 | 537 | } |
| 507 | 538 | const projectThreadIds = threadIdsByProjectId[nextProjectId] ?? EMPTY_THREAD_IDS; |
| 508 | | const nextProjectThreadIds = appendId(projectThreadIds, nextThread.id); |
| 539 | const nextProjectThreadIds = appendId(projectThreadIds, threadId); |
| 509 | 540 | if (!arraysEqual(projectThreadIds, nextProjectThreadIds)) { |
| 510 | 541 | threadIdsByProjectId = { |
| 511 | 542 | ...threadIdsByProjectId, |
| ... | ... | @@ -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 | 584 | if (!threadShellsEqual(previousShell, nextShell)) { |
| 524 | 585 | nextState = { |
| 525 | 586 | ...nextState, |
| ... | ... | @@ -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 | 595 | nextState = { |
| 535 | 596 | ...nextState, |
| 536 | 597 | threadSessionById: { |
| ... | ... | @@ -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 | 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 | 688 | function writeThreadShellState( |
| 627 | 689 | state: EnvironmentState, |
| 628 | 690 | nextThread: { |
| ... | ... | @@ -632,49 +694,14 @@ function writeThreadShellState( |
| 632 | 694 | summary: SidebarThreadSummary; |
| 633 | 695 | }, |
| 634 | 696 | ): EnvironmentState { |
| 635 | | let nextState = state; |
| 636 | 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)) { |
| 641 | | nextState = { |
| 642 | | ...nextState, |
| 643 | | threadIds: [...nextState.threadIds, nextThread.shell.id], |
| 644 | | }; |
| 645 | | } |
| 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 | | } |
| 699 | let nextState = ensureThreadRegistered( |
| 700 | state, |
| 701 | nextThread.shell.id, |
| 702 | nextThread.shell.projectId, |
| 703 | previousShell?.projectId, |
| 704 | ); |
| 678 | 705 | |
| 679 | 706 | if (!threadShellsEqual(previousShell, nextThread.shell)) { |
| 680 | 707 | nextState = { |
| ... | ... | @@ -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 | 719 | nextState = { |
| 691 | 720 | ...nextState, |
| 692 | 721 | threadSessionById: { |
| ... | ... | @@ -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 | 1041 | function getStoredEnvironmentState( |
| 1089 | 1042 | state: AppState, |
| 1090 | 1043 | environmentId: EnvironmentId, |
| ... | ... | @@ -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 | 1072 | function syncEnvironmentShellSnapshot( |
| 1139 | 1073 | state: EnvironmentState, |
| 1140 | 1074 | snapshot: OrchestrationShellSnapshot, |
| ... | ... | @@ -1175,22 +1109,6 @@ function syncEnvironmentShellSnapshot( |
| 1175 | 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 | 1112 | export function syncServerShellSnapshot( |
| 1195 | 1113 | state: AppState, |
| 1196 | 1114 | snapshot: OrchestrationShellSnapshot, |
| ... | ... | @@ -2007,7 +1925,6 @@ export function setThreadBranch( |
| 2007 | 1925 | |
| 2008 | 1926 | interface AppStore extends AppState { |
| 2009 | 1927 | setActiveEnvironmentId: (environmentId: EnvironmentId) => void; |
| 2010 | | syncServerReadModel: (readModel: OrchestrationReadModel, environmentId: EnvironmentId) => void; |
| 2011 | 1928 | syncServerShellSnapshot: ( |
| 2012 | 1929 | snapshot: OrchestrationShellSnapshot, |
| 2013 | 1930 | environmentId: EnvironmentId, |
| ... | ... | @@ -2031,8 +1948,6 @@ export const useStore = create<AppStore>((set) => ({ |
| 2031 | 1948 | ...initialState, |
| 2032 | 1949 | setActiveEnvironmentId: (environmentId) => |
| 2033 | 1950 | set((state) => setActiveEnvironmentId(state, environmentId)), |
| 2034 | | syncServerReadModel: (readModel, environmentId) => |
| 2035 | | set((state) => syncServerReadModel(state, readModel, environmentId)), |
| 2036 | 1951 | syncServerShellSnapshot: (snapshot, environmentId) => |
| 2037 | 1952 | set((state) => syncServerShellSnapshot(state, snapshot, environmentId)), |
| 2038 | 1953 | syncServerThreadDetail: (thread, environmentId) => |