@opengeni/react 3.1.0 → 3.1.3-canary.38890816741001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +769 -22
- package/dist/accounts.js +89 -11
- package/dist/accounts.js.map +1 -1
- package/dist/approvals.d.ts +2 -1
- package/dist/artifacts-document.js +5 -3
- package/dist/artifacts-presentation.js +5 -3
- package/dist/artifacts-spreadsheet.d.ts +1 -1
- package/dist/artifacts-spreadsheet.js +5 -3
- package/dist/artifacts.d.ts +5 -1
- package/dist/artifacts.js +536 -42
- package/dist/artifacts.js.map +1 -1
- package/dist/browser-viewer-ZWR6MWJ7.js +19 -0
- package/dist/capability-catalog-row.d.ts +20 -0
- package/dist/capability-logo-fallback.d.ts +3 -0
- package/dist/chat-interactive-block-BO2IWXVK.js +14 -0
- package/dist/chunk-2DDJ6YH6.js +41 -0
- package/dist/chunk-2DDJ6YH6.js.map +1 -0
- package/dist/chunk-2HVLQ26H.js +183 -0
- package/dist/chunk-2HVLQ26H.js.map +1 -0
- package/dist/{chunk-RGALCTWO.js → chunk-2I6R3GCZ.js} +4285 -3980
- package/dist/chunk-2I6R3GCZ.js.map +1 -0
- package/dist/{chunk-D3UIAJKY.js → chunk-2SFFYIH3.js} +1179 -166
- package/dist/chunk-2SFFYIH3.js.map +1 -0
- package/dist/chunk-3PVDYTTB.js +24 -0
- package/dist/chunk-3PVDYTTB.js.map +1 -0
- package/dist/chunk-3UZCWK46.js +298 -0
- package/dist/chunk-3UZCWK46.js.map +1 -0
- package/dist/chunk-47CKUXTW.js +293 -0
- package/dist/chunk-47CKUXTW.js.map +1 -0
- package/dist/chunk-4QYP6QUI.js +109 -0
- package/dist/chunk-4QYP6QUI.js.map +1 -0
- package/dist/chunk-5EY26VZ7.js +1053 -0
- package/dist/chunk-5EY26VZ7.js.map +1 -0
- package/dist/chunk-5I4VRWBB.js +56 -0
- package/dist/chunk-5I4VRWBB.js.map +1 -0
- package/dist/chunk-5WOR7UCL.js +191 -0
- package/dist/chunk-5WOR7UCL.js.map +1 -0
- package/dist/chunk-5YJLGOM5.js +51 -0
- package/dist/chunk-5YJLGOM5.js.map +1 -0
- package/dist/chunk-6JFN3OWG.js +386 -0
- package/dist/chunk-6JFN3OWG.js.map +1 -0
- package/dist/chunk-7QJT5O4S.js +35 -0
- package/dist/chunk-7QJT5O4S.js.map +1 -0
- package/dist/chunk-A55KV72M.js +402 -0
- package/dist/chunk-A55KV72M.js.map +1 -0
- package/dist/chunk-ADWCLI4G.js +2028 -0
- package/dist/chunk-ADWCLI4G.js.map +1 -0
- package/dist/chunk-B27E3V37.js +29 -0
- package/dist/chunk-B27E3V37.js.map +1 -0
- package/dist/chunk-B7TC2A33.js +83 -0
- package/dist/chunk-B7TC2A33.js.map +1 -0
- package/dist/chunk-BLL56F7R.js +385 -0
- package/dist/chunk-BLL56F7R.js.map +1 -0
- package/dist/chunk-BOFEQC6K.js +162 -0
- package/dist/chunk-BOFEQC6K.js.map +1 -0
- package/dist/chunk-CM7LVUKC.js +35 -0
- package/dist/chunk-CM7LVUKC.js.map +1 -0
- package/dist/chunk-CORUP6JB.js +122 -0
- package/dist/chunk-CORUP6JB.js.map +1 -0
- package/dist/chunk-CSX5FFKZ.js +35 -0
- package/dist/chunk-CSX5FFKZ.js.map +1 -0
- package/dist/chunk-EGVHJKGJ.js +3125 -0
- package/dist/chunk-EGVHJKGJ.js.map +1 -0
- package/dist/{chunk-NEXLLTP4.js → chunk-G76LSA76.js} +6 -5
- package/dist/chunk-G76LSA76.js.map +1 -0
- package/dist/{chunk-PEI6RANI.js → chunk-GQMRL52F.js} +311 -75
- package/dist/chunk-GQMRL52F.js.map +1 -0
- package/dist/{chunk-ELIYWBZR.js → chunk-GUUXOISA.js} +1113 -457
- package/dist/chunk-GUUXOISA.js.map +1 -0
- package/dist/{chunk-HUIKIDM5.js → chunk-I7X2P4FU.js} +225 -116
- package/dist/chunk-I7X2P4FU.js.map +1 -0
- package/dist/chunk-IGFZFLI2.js +1390 -0
- package/dist/chunk-IGFZFLI2.js.map +1 -0
- package/dist/{chunk-4MDLEHXL.js → chunk-IMSUVYQB.js} +26 -13
- package/dist/chunk-IMSUVYQB.js.map +1 -0
- package/dist/chunk-IOYHDEP3.js +38 -0
- package/dist/chunk-IOYHDEP3.js.map +1 -0
- package/dist/chunk-IZV5FTJC.js +55 -0
- package/dist/chunk-IZV5FTJC.js.map +1 -0
- package/dist/chunk-KF33GTWN.js +343 -0
- package/dist/chunk-KF33GTWN.js.map +1 -0
- package/dist/chunk-KSGQ5ODA.js +457 -0
- package/dist/chunk-KSGQ5ODA.js.map +1 -0
- package/dist/{chunk-OAM4WAAM.js → chunk-KZC6QRNR.js} +1513 -697
- package/dist/chunk-KZC6QRNR.js.map +1 -0
- package/dist/chunk-LJUMYUBO.js +218 -0
- package/dist/chunk-LJUMYUBO.js.map +1 -0
- package/dist/chunk-LSW2OJVV.js +380 -0
- package/dist/chunk-LSW2OJVV.js.map +1 -0
- package/dist/chunk-M357MHH3.js +2 -0
- package/dist/chunk-M357MHH3.js.map +1 -0
- package/dist/chunk-MK2TQO4Y.js +24 -0
- package/dist/chunk-MK2TQO4Y.js.map +1 -0
- package/dist/chunk-N7S72EXK.js +25 -0
- package/dist/chunk-N7S72EXK.js.map +1 -0
- package/dist/{chunk-XXWH2JLC.js → chunk-NKMYZFHA.js} +616 -621
- package/dist/chunk-NKMYZFHA.js.map +1 -0
- package/dist/chunk-NX4ADH73.js +14 -0
- package/dist/chunk-NX4ADH73.js.map +1 -0
- package/dist/chunk-O7F6UJO4.js +71 -0
- package/dist/chunk-O7F6UJO4.js.map +1 -0
- package/dist/chunk-OVB5WIG5.js +14064 -0
- package/dist/chunk-OVB5WIG5.js.map +1 -0
- package/dist/chunk-OYQQJL3J.js +16 -0
- package/dist/chunk-OYQQJL3J.js.map +1 -0
- package/dist/{chunk-H5YGYNXS.js → chunk-P4BNUO57.js} +60 -17
- package/dist/chunk-P4BNUO57.js.map +1 -0
- package/dist/{chunk-JVYQRRC4.js → chunk-PEHTX3V7.js} +21 -1
- package/dist/chunk-PEHTX3V7.js.map +1 -0
- package/dist/chunk-PMV53IMM.js +26 -0
- package/dist/chunk-PMV53IMM.js.map +1 -0
- package/dist/{chunk-KCGXPP5V.js → chunk-PYEOPA4M.js} +13 -7
- package/dist/chunk-PYEOPA4M.js.map +1 -0
- package/dist/{chunk-KZ73RL76.js → chunk-QHUE7E4N.js} +8 -3
- package/dist/chunk-QHUE7E4N.js.map +1 -0
- package/dist/chunk-QV3DJVRZ.js +11 -0
- package/dist/chunk-QV3DJVRZ.js.map +1 -0
- package/dist/chunk-R5SBNG7S.js +810 -0
- package/dist/chunk-R5SBNG7S.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/chunk-S2QH6EZ5.js +211 -0
- package/dist/chunk-S2QH6EZ5.js.map +1 -0
- package/dist/chunk-SHLZMAJN.js +304 -0
- package/dist/chunk-SHLZMAJN.js.map +1 -0
- package/dist/chunk-TGXK4BAZ.js +293 -0
- package/dist/chunk-TGXK4BAZ.js.map +1 -0
- package/dist/{chunk-5YK2ZDIG.js → chunk-TNEZNMSN.js} +4 -10
- package/dist/chunk-TNEZNMSN.js.map +1 -0
- package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
- package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
- package/dist/chunk-UNJPZGNK.js +63 -0
- package/dist/chunk-UNJPZGNK.js.map +1 -0
- package/dist/{chunk-MZ2G2XBA.js → chunk-V5ZUJT2T.js} +205 -45
- package/dist/chunk-V5ZUJT2T.js.map +1 -0
- package/dist/chunk-VIUEOHXY.js +394 -0
- package/dist/chunk-VIUEOHXY.js.map +1 -0
- package/dist/chunk-WIUKB3LD.js +1911 -0
- package/dist/chunk-WIUKB3LD.js.map +1 -0
- package/dist/{chunk-ICA6R6TW.js → chunk-WNN7NO2N.js} +1451 -355
- package/dist/chunk-WNN7NO2N.js.map +1 -0
- package/dist/{chunk-YJFGAFBD.js → chunk-X5UVUIMF.js} +1892 -1223
- package/dist/chunk-X5UVUIMF.js.map +1 -0
- package/dist/chunk-XDA5QJE7.js +402 -0
- package/dist/chunk-XDA5QJE7.js.map +1 -0
- package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
- package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
- package/dist/chunk-XHF7OYAN.js +1150 -0
- package/dist/chunk-XHF7OYAN.js.map +1 -0
- package/dist/chunk-YSRBFJMQ.js +43 -0
- package/dist/chunk-YSRBFJMQ.js.map +1 -0
- package/dist/chunk-ZOZAPAUO.js +174 -0
- package/dist/chunk-ZOZAPAUO.js.map +1 -0
- package/dist/chunk-ZQV5W7YC.js +809 -0
- package/dist/chunk-ZQV5W7YC.js.map +1 -0
- package/dist/chunk-ZUWF4NF7.js +13 -0
- package/dist/chunk-ZUWF4NF7.js.map +1 -0
- package/dist/client.d.ts +16 -7
- package/dist/clipboard.js +3 -1
- package/dist/commands/types.d.ts +2 -0
- package/dist/components/anthropic-mark.d.ts +3 -0
- package/dist/components/approval-surface.d.ts +19 -6
- package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
- package/dist/components/artifacts/artifact-sandbox.d.ts +30 -0
- package/dist/components/artifacts/artifact-surface.d.ts +4 -2
- package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
- package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
- package/dist/components/artifacts/document-editor.d.ts +4 -2
- package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
- package/dist/components/artifacts/editable-artifact-view.d.ts +48 -0
- package/dist/components/artifacts/editable-document.d.ts +1 -1
- package/dist/components/artifacts/editable-presentation.d.ts +2 -2
- package/dist/components/artifacts/editable-spreadsheet.d.ts +8 -3
- package/dist/components/artifacts/index.d.ts +9 -2
- package/dist/components/artifacts/inline-html-document.d.ts +2 -0
- package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
- package/dist/components/artifacts/presentation-editor.d.ts +6 -2
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +23 -3
- package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
- package/dist/components/artifacts/site-view.d.ts +26 -0
- package/dist/components/artifacts/spreadsheet-command-queue.d.ts +20 -0
- package/dist/components/artifacts/spreadsheet-download.d.ts +7 -0
- package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +10 -3
- package/dist/components/artifacts/spreadsheet-resize.d.ts +50 -0
- package/dist/components/artifacts/spreadsheet-submissions.d.ts +9 -0
- package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
- package/dist/components/browser-select-control.d.ts +16 -0
- package/dist/components/browser-viewer.d.ts +11 -2
- package/dist/components/chat-composer.d.ts +21 -3
- package/dist/components/child-session-link.d.ts +9 -0
- package/dist/components/claude-mark.d.ts +3 -0
- package/dist/components/code-editor.d.ts +1 -4
- package/dist/components/composer-transcription-control.d.ts +7 -0
- package/dist/components/composer.d.ts +34 -67
- package/dist/components/computer-viewer.d.ts +1 -1
- package/dist/components/copy-button.d.ts +3 -1
- package/dist/components/desktop-viewer.d.ts +1 -1
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/grok-mark.d.ts +26 -0
- package/dist/components/human-input-form.d.ts +28 -32
- package/dist/components/human-input-surface.d.ts +7 -2
- package/dist/components/linkified-text.d.ts +18 -0
- package/dist/components/machine-input-display.d.ts +22 -1
- package/dist/components/machines/agent-status-icon.d.ts +6 -0
- package/dist/components/machines/machine-detail.d.ts +7 -1
- package/dist/components/machines/machine-status.d.ts +48 -0
- package/dist/components/markdown-table-layout.d.ts +18 -0
- package/dist/components/markdown.d.ts +37 -4
- package/dist/components/message-timeline.d.ts +86 -7
- package/dist/components/model-mark.d.ts +29 -0
- package/dist/components/model-policy-picker-menu.d.ts +9 -0
- package/dist/components/model-policy-picker.d.ts +41 -25
- package/dist/components/new-conversation.d.ts +32 -0
- package/dist/components/open-geni-chat.d.ts +73 -0
- package/dist/components/open-geni-links.d.ts +68 -0
- package/dist/components/openrouter-mark.d.ts +6 -0
- package/dist/components/opper-mark.d.ts +7 -0
- package/dist/components/preview-loading.d.ts +2 -0
- package/dist/components/provider-recovery-notice.d.ts +16 -0
- package/dist/components/queue-item-content.d.ts +9 -0
- package/dist/components/rehype-secret-refs.d.ts +23 -0
- package/dist/components/remark-soft-line-breaks.d.ts +38 -0
- package/dist/components/sandbox-files.d.ts +3 -1
- package/dist/components/sandbox-workspace.d.ts +18 -0
- package/dist/components/secret-question-field.d.ts +31 -0
- package/dist/components/secret-ref-chip.d.ts +35 -0
- package/dist/components/session-capability-frame.d.ts +37 -0
- package/dist/components/session-chrome.d.ts +57 -8
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-commands.d.ts +6 -0
- package/dist/components/session-connection-request.d.ts +8 -0
- package/dist/components/session-conversation.d.ts +157 -0
- package/dist/components/session-list.d.ts +39 -0
- package/dist/components/session-mcp-capability-card.d.ts +41 -0
- package/dist/components/session-proxy-scope.d.ts +35 -0
- package/dist/components/session-status.d.ts +4 -1
- package/dist/components/session-usage-limit-notice.d.ts +39 -0
- package/dist/components/timeline-anchor.d.ts +24 -0
- package/dist/components/timeline-annotation-chrome.d.ts +25 -0
- package/dist/components/timeline-annotation-layout.d.ts +42 -0
- package/dist/components/timeline-annotation-markers.d.ts +8 -0
- package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
- package/dist/components/timeline-annotation-shared.d.ts +31 -0
- package/dist/components/timeline-annotations-dialog.d.ts +3 -2
- package/dist/components/timeline-annotations.d.ts +13 -3
- package/dist/components/timeline-reading-progress.d.ts +11 -0
- package/dist/components/timeline-search.d.ts +34 -0
- package/dist/components/timeline-settlement.d.ts +16 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/components/tool-action-review.d.ts +47 -0
- package/dist/components/tool-review-history.d.ts +25 -0
- package/dist/components/usage-limit-notice.d.ts +49 -0
- package/dist/components/usage-member-list.d.ts +91 -0
- package/dist/components/usage-meter.d.ts +73 -0
- package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
- package/dist/components/user-message-body.d.ts +10 -1
- package/dist/composer-messages.d.ts +74 -0
- package/dist/composer-messages.js +9 -0
- package/dist/composer-messages.js.map +1 -0
- package/dist/composer-voice-events.d.ts +8 -0
- package/dist/composer.d.ts +5 -4
- package/dist/composer.js +26 -13
- package/dist/computer-viewer-GMLDXPON.js +20 -0
- package/dist/computer-viewer-GMLDXPON.js.map +1 -0
- package/dist/connect-account-identity.d.ts +8 -0
- package/dist/connect-accounts.d.ts +15 -0
- package/dist/connect-chooser.d.ts +18 -0
- package/dist/connect-panel.d.ts +18 -0
- package/dist/connect-setup.d.ts +18 -0
- package/dist/connect.d.ts +21 -0
- package/dist/connect.js +2149 -0
- package/dist/connect.js.map +1 -0
- package/dist/connection-catalog.d.ts +38 -0
- package/dist/connection-discovery.d.ts +12 -0
- package/dist/connection-installed.d.ts +17 -0
- package/dist/connection-logo.d.ts +9 -0
- package/dist/connection-service-logo.d.ts +8 -0
- package/dist/connection-service-presentation.d.ts +13 -0
- package/dist/connection-skeleton.d.ts +5 -0
- package/dist/connection-type-picker.d.ts +6 -0
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/desktop.d.ts +7 -0
- package/dist/desktop.js +27 -0
- package/dist/desktop.js.map +1 -0
- package/dist/device-authorization.d.ts +23 -0
- package/dist/diffs.d.ts +8 -0
- package/dist/diffs.js +15 -0
- package/dist/diffs.js.map +1 -0
- package/dist/discovery-cache.d.ts +5 -0
- package/dist/editor.d.ts +7 -0
- package/dist/editor.js +22 -0
- package/dist/editor.js.map +1 -0
- package/dist/embedded-new-voice-OCRNT6U5.js +18 -0
- package/dist/embedded-new-voice-OCRNT6U5.js.map +1 -0
- package/dist/embedded-voice-RPHPYVBT.js +18 -0
- package/dist/embedded-voice-RPHPYVBT.js.map +1 -0
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-SI63O5V4.js} +4 -1
- package/dist/fleet-decision-row-SI63O5V4.js.map +1 -0
- package/dist/hooks/latest-question.d.ts +25 -0
- package/dist/hooks/shared-machines-list.d.ts +4 -0
- package/dist/hooks/shared-query.d.ts +113 -0
- package/dist/hooks/use-attached-browsers.d.ts +1 -1
- package/dist/hooks/use-auth-runs.d.ts +1 -4
- package/dist/hooks/use-available-models.d.ts +1 -1
- package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
- package/dist/hooks/use-browser-session.d.ts +12 -1
- package/dist/hooks/use-client-config-flags.d.ts +49 -0
- package/dist/hooks/use-codex-accounts.d.ts +59 -10
- package/dist/hooks/use-composer.d.ts +24 -2
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-input-posture.d.ts +13 -0
- package/dist/hooks/use-computer-session.d.ts +4 -0
- package/dist/hooks/use-connect.d.ts +23 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +12 -0
- package/dist/hooks/use-goal.d.ts +2 -1
- package/dist/hooks/use-last-started-turn-policy.d.ts +5 -1
- package/dist/hooks/use-new-conversation.d.ts +56 -0
- package/dist/hooks/use-new-session-draft.d.ts +74 -0
- package/dist/hooks/use-realtime-voice-models.d.ts +7 -0
- package/dist/hooks/use-sandbox-files.d.ts +4 -1
- package/dist/hooks/use-session-conversation.d.ts +49 -0
- package/dist/hooks/use-session-events.d.ts +28 -7
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session-model-account.d.ts +48 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +7 -1
- package/dist/hooks/use-terminal-stream.d.ts +11 -5
- package/dist/hooks/use-usage.d.ts +40 -0
- package/dist/hooks/use-vaults.d.ts +53 -0
- package/dist/hooks/use-voice-input.d.ts +8 -1
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/hooks/use-workspace-sessions.d.ts +22 -5
- package/dist/human-input-model.d.ts +122 -0
- package/dist/identity-link-accounts.d.ts +11 -0
- package/dist/identity-link-consent.d.ts +18 -0
- package/dist/index.d.ts +82 -20
- package/dist/index.js +2350 -6605
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +1 -1
- package/dist/interaction.js +14 -24
- package/dist/interaction.js.map +1 -1
- package/dist/latest-question-R77KWSUP.js +118 -0
- package/dist/latest-question-R77KWSUP.js.map +1 -0
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/error-message.d.ts +7 -0
- package/dist/lib/format.d.ts +58 -4
- package/dist/lib/host-theme.d.ts +77 -0
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/lib/menu-styles.d.ts +22 -0
- package/dist/lib/notify-observer.d.ts +2 -0
- package/dist/lib/pierre-diffs-loader.d.ts +20 -0
- package/dist/lib/provider-recovery.d.ts +55 -0
- package/dist/lib/scrollbars.d.ts +11 -0
- package/dist/lib/terminal-input-readiness.d.ts +11 -0
- package/dist/lib/transient-retry.d.ts +25 -0
- package/dist/lib/workbench-peers.d.ts +36 -0
- package/dist/lib/xterm-theme.d.ts +7 -5
- package/dist/machines.js +9 -5
- package/dist/markdown-table-layout-3PV2UAAH.js +113 -0
- package/dist/markdown-table-layout-3PV2UAAH.js.map +1 -0
- package/dist/mcp-connection-status.d.ts +14 -0
- package/dist/model-mark-paths.d.ts +34 -0
- package/dist/model-mark-paths.js +16 -0
- package/dist/model-mark-paths.js.map +1 -0
- package/dist/model-picker-model.d.ts +111 -0
- package/dist/model-picker-model.js +25 -0
- package/dist/model-picker-model.js.map +1 -0
- package/dist/model-policy-picker-menu-7GJ5URPR.js +17 -0
- package/dist/model-policy-picker-menu-7GJ5URPR.js.map +1 -0
- package/dist/model-policy.d.ts +30 -3
- package/dist/model-policy.js +14 -1
- package/dist/native-previews.d.ts +28 -0
- package/dist/native-previews.js +82 -0
- package/dist/native-previews.js.map +1 -0
- package/dist/new-session-draft.d.ts +7 -0
- package/dist/new-session-draft.js +639 -0
- package/dist/new-session-draft.js.map +1 -0
- package/dist/notification-text.d.ts +27 -0
- package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
- package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
- package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
- package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
- package/dist/older-history.d.ts +3 -2
- package/dist/organization-model.d.ts +32 -0
- package/dist/organization-model.js +65 -0
- package/dist/organization-model.js.map +1 -0
- package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-5OCKOQWZ.js} +16 -78
- package/dist/platform-activity-row-5OCKOQWZ.js.map +1 -0
- package/dist/plugin-details.d.ts +14 -0
- package/dist/plugin-discovery.d.ts +25 -0
- package/dist/preview-loading-paint.d.ts +20 -0
- package/dist/provider.d.ts +25 -2
- package/dist/queue-presentation-model.d.ts +72 -0
- package/dist/queue-presentation-model.js +38 -0
- package/dist/queue-presentation-model.js.map +1 -0
- package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-GJBH7NKO.js} +65 -265
- package/dist/queue-surface-implementation-GJBH7NKO.js.map +1 -0
- package/dist/realtime/embedded-new-voice.d.ts +1 -0
- package/dist/realtime/embedded-voice.d.ts +1 -0
- package/dist/realtime/realtime-control.d.ts +4 -65
- package/dist/realtime/session-realtime.d.ts +78 -0
- package/dist/realtime.js +25 -1294
- package/dist/realtime.js.map +1 -1
- package/dist/sandbox-files-O27IPOZW.js +21 -0
- package/dist/sandbox-files-O27IPOZW.js.map +1 -0
- package/dist/sandbox-label-model.d.ts +30 -0
- package/dist/sandbox-label-model.js +20 -0
- package/dist/sandbox-label-model.js.map +1 -0
- package/dist/scrollbars.d.ts +1 -0
- package/dist/scrollbars.js +10 -0
- package/dist/scrollbars.js.map +1 -0
- package/dist/secret-reference-label.d.ts +14 -0
- package/dist/secrets-ui.d.ts +57 -0
- package/dist/session-agents-model.d.ts +10 -0
- package/dist/session-agents-model.js +25 -0
- package/dist/session-agents-model.js.map +1 -0
- package/dist/session-attention-model.d.ts +8 -0
- package/dist/session-attention-model.js +47 -0
- package/dist/session-attention-model.js.map +1 -0
- package/dist/session-auth-recommendation.d.ts +5 -0
- package/dist/session-capability-policy.d.ts +31 -0
- package/dist/session-feedback-model.d.ts +20 -0
- package/dist/session-feedback-model.js +56 -0
- package/dist/session-feedback-model.js.map +1 -0
- package/dist/session-list-model.d.ts +138 -0
- package/dist/session-list-model.js +274 -0
- package/dist/session-list-model.js.map +1 -0
- package/dist/session-realtime-7DJOIHQW.js +21 -0
- package/dist/session-realtime-7DJOIHQW.js.map +1 -0
- package/dist/session-realtime.d.ts +7 -0
- package/dist/session-realtime.js +19 -0
- package/dist/session-realtime.js.map +1 -0
- package/dist/session-status-model.d.ts +36 -0
- package/dist/session-ui.d.ts +39 -1
- package/dist/session-ui.js +126 -12
- package/dist/session-ui.js.map +1 -1
- package/dist/session.d.ts +8 -2
- package/dist/session.js +35 -20
- package/dist/site-snapshot.d.ts +14 -0
- package/dist/sites-ui.d.ts +24 -0
- package/dist/sites.d.ts +3 -0
- package/dist/sites.js +14 -0
- package/dist/sites.js.map +1 -0
- package/dist/skill-discovery.d.ts +47 -0
- package/dist/terminal.d.ts +8 -0
- package/dist/terminal.js +32 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing/session-scenarios.d.ts +10 -0
- package/dist/testing/session-scenarios.js +367 -0
- package/dist/testing/session-scenarios.js.map +1 -0
- package/dist/timeline/activity-rail.d.ts +11 -2
- package/dist/timeline/agent-identity.d.ts +30 -0
- package/dist/timeline/agent-row.d.ts +31 -0
- package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
- package/dist/timeline/compaction-copy.d.ts +46 -0
- package/dist/timeline/entrance.d.ts +13 -1
- package/dist/timeline/focus-sequence.d.ts +9 -0
- package/dist/timeline/genie-copy.d.ts +3 -0
- package/dist/timeline/genie-loading.d.ts +47 -0
- package/dist/timeline/index.d.ts +10 -5
- package/dist/timeline/knowledge-receipt.d.ts +17 -0
- package/dist/timeline/message-sender.d.ts +19 -0
- package/dist/timeline/notice-presentation.d.ts +13 -0
- package/dist/timeline/parsers.d.ts +11 -0
- package/dist/timeline/patch-apply-command.d.ts +18 -0
- package/dist/timeline/platform-activity-presentation.d.ts +21 -0
- package/dist/timeline/platform-activity-row.d.ts +1 -2
- package/dist/timeline/presented-image.d.ts +11 -0
- package/dist/timeline/projection.d.ts +31 -2
- package/dist/timeline/provider-logo.d.ts +15 -0
- package/dist/timeline/question-nav-model.d.ts +23 -0
- package/dist/timeline/registry.d.ts +12 -1
- package/dist/timeline/retained-image.d.ts +18 -0
- package/dist/timeline/retained-loaders.d.ts +27 -0
- package/dist/timeline/rolling-activity.d.ts +11 -0
- package/dist/timeline/screenshot-lightbox.d.ts +23 -1
- package/dist/timeline/session-usage-limit-row.d.ts +11 -0
- package/dist/timeline/shared.d.ts +4 -1
- package/dist/timeline/startup-dispatch-details.d.ts +5 -0
- package/dist/timeline/startup-preference.d.ts +3 -0
- package/dist/timeline/startup-timings.d.ts +4 -0
- package/dist/timeline/tool-display-name.d.ts +5 -2
- package/dist/timeline/tool-presentation.d.ts +172 -0
- package/dist/timeline/turn-summary-model.d.ts +95 -0
- package/dist/timeline/turn-summary.d.ts +20 -54
- package/dist/timeline/types.d.ts +179 -7
- package/dist/timeline/work-presentation.d.ts +60 -0
- package/dist/timeline-annotation-markers-U3G66KUD.js +166 -0
- package/dist/timeline-annotation-markers-U3G66KUD.js.map +1 -0
- package/dist/timeline-annotation-selection-BI7QTPTZ.js +242 -0
- package/dist/timeline-annotation-selection-BI7QTPTZ.js.map +1 -0
- package/dist/timeline-annotations-dialog-FOMHSDTE.js +274 -0
- package/dist/timeline-annotations-dialog-FOMHSDTE.js.map +1 -0
- package/dist/timeline-model.d.ts +29 -0
- package/dist/timeline-model.js +254 -0
- package/dist/timeline-model.js.map +1 -0
- package/dist/usage/allowance-copy.d.ts +25 -0
- package/dist/usage/session-usage-limit.d.ts +88 -0
- package/dist/usage/summary.d.ts +35 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +1187 -0
- package/dist/usage.js.map +1 -0
- package/dist/voice-recording-owner.d.ts +7 -0
- package/package.json +107 -7
- package/src/accounts.tsx +116 -14
- package/src/approvals.ts +8 -1
- package/src/artifacts-spreadsheet.ts +1 -0
- package/src/artifacts.ts +44 -0
- package/src/assets/README.md +8 -0
- package/src/assets/gmail.ico +0 -0
- package/src/assets/outlook.ico +0 -0
- package/src/capability-catalog-row.tsx +94 -0
- package/src/capability-logo-fallback.ts +22 -0
- package/src/client.ts +20 -31
- package/src/commands/registry.ts +18 -15
- package/src/commands/types.ts +2 -0
- package/src/components/anthropic-mark.tsx +10 -0
- package/src/components/approval-surface.tsx +401 -138
- package/src/components/artifacts/artifact-chrome.tsx +413 -0
- package/src/components/artifacts/artifact-sandbox.tsx +189 -0
- package/src/components/artifacts/artifact-surface.tsx +40 -20
- package/src/components/artifacts/chat-interactive-block.tsx +260 -0
- package/src/components/artifacts/deferred-chat-media.tsx +67 -0
- package/src/components/artifacts/document-editor.tsx +141 -16
- package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
- package/src/components/artifacts/editable-artifact-view.tsx +197 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
- package/src/components/artifacts/editable-document.tsx +37 -36
- package/src/components/artifacts/editable-presentation.tsx +36 -18
- package/src/components/artifacts/editable-spreadsheet.tsx +543 -103
- package/src/components/artifacts/index.ts +45 -0
- package/src/components/artifacts/inline-html-document.ts +24 -0
- package/src/components/artifacts/latest-task-runner.ts +49 -0
- package/src/components/artifacts/presentation-editor.tsx +323 -67
- package/src/components/artifacts/published-html-artifact-frame.tsx +122 -5
- package/src/components/artifacts/session-artifact-viewer.tsx +378 -0
- package/src/components/artifacts/site-view.tsx +121 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
- package/src/components/artifacts/spreadsheet-command-queue.ts +184 -0
- package/src/components/artifacts/spreadsheet-download.tsx +109 -0
- package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +344 -144
- package/src/components/artifacts/spreadsheet-resize.tsx +421 -0
- package/src/components/artifacts/spreadsheet-submissions.tsx +46 -0
- package/src/components/artifacts/visualization-assets/base.css +1015 -0
- package/src/components/artifacts/visualization-assets/helpers.html +349 -0
- package/src/components/artifacts/visualization-assets.gen.ts +3 -0
- package/src/components/browser-select-control.tsx +312 -0
- package/src/components/browser-viewer.tsx +778 -318
- package/src/components/chat-composer.tsx +47 -5
- package/src/components/child-session-link.tsx +36 -0
- package/src/components/claude-mark.tsx +11 -0
- package/src/components/code-editor.tsx +13 -16
- package/src/components/composer-transcription-control.tsx +128 -30
- package/src/components/composer.tsx +404 -195
- package/src/components/computer-viewer.tsx +880 -253
- package/src/components/copy-button.tsx +13 -5
- package/src/components/desktop-viewer.tsx +1 -1
- package/src/components/enrollment-consent.tsx +1 -1
- package/src/components/enrollment-device-flow.tsx +1 -1
- package/src/components/file-browser.tsx +10 -14
- package/src/components/fleet-tile.tsx +9 -18
- package/src/components/generated-video-player.tsx +4 -2
- package/src/components/grok-mark.tsx +34 -0
- package/src/components/human-input-form.tsx +590 -283
- package/src/components/human-input-surface.tsx +11 -0
- package/src/components/interaction-intervention-banner.tsx +2 -2
- package/src/components/linkified-text.tsx +79 -0
- package/src/components/machine-card.tsx +35 -78
- package/src/components/machine-dock-bar.tsx +1 -1
- package/src/components/machine-input-display.ts +197 -3
- package/src/components/machines/agent-status-icon.tsx +31 -0
- package/src/components/machines/machine-detail.tsx +200 -31
- package/src/components/machines/machine-operation-policy.tsx +1 -1
- package/src/components/machines/machine-status.ts +241 -0
- package/src/components/machines-dashboard.tsx +2 -17
- package/src/components/markdown-table-layout.ts +143 -0
- package/src/components/markdown.tsx +466 -129
- package/src/components/message-timeline.tsx +2567 -728
- package/src/components/model-mark.tsx +102 -0
- package/src/components/model-picker.tsx +15 -4
- package/src/components/model-policy-picker-menu.tsx +371 -0
- package/src/components/model-policy-picker.tsx +256 -412
- package/src/components/new-conversation.tsx +221 -0
- package/src/components/open-geni-chat.tsx +309 -0
- package/src/components/open-geni-links.tsx +179 -0
- package/src/components/openrouter-mark.tsx +13 -0
- package/src/components/opper-mark.tsx +18 -0
- package/src/components/pierre-diff.tsx +41 -16
- package/src/components/pierre-file.tsx +35 -10
- package/src/components/preview-loading.tsx +109 -0
- package/src/components/provider-recovery-notice.tsx +49 -0
- package/src/components/queue-item-content.ts +14 -0
- package/src/components/queue-surface-implementation.tsx +51 -356
- package/src/components/queue-surface-state.tsx +3 -1
- package/src/components/rehype-secret-refs.ts +98 -0
- package/src/components/remark-soft-line-breaks.ts +166 -0
- package/src/components/sandbox-files.tsx +91 -15
- package/src/components/sandbox-terminal.tsx +84 -13
- package/src/components/sandbox-workspace.tsx +147 -51
- package/src/components/secret-question-field.tsx +324 -0
- package/src/components/secret-ref-chip.tsx +185 -0
- package/src/components/session-capability-frame.tsx +197 -0
- package/src/components/session-chrome.tsx +842 -251
- package/src/components/session-commands-panel.tsx +126 -0
- package/src/components/session-commands.tsx +18 -0
- package/src/components/session-connection-request.tsx +97 -0
- package/src/components/session-conversation.tsx +876 -0
- package/src/components/session-list.tsx +257 -0
- package/src/components/session-mcp-capability-card.tsx +680 -0
- package/src/components/session-proxy-scope.tsx +153 -0
- package/src/components/session-status.tsx +26 -17
- package/src/components/session-usage-limit-notice.tsx +174 -0
- package/src/components/timeline-anchor.tsx +119 -0
- package/src/components/timeline-annotation-chrome.tsx +196 -0
- package/src/components/timeline-annotation-layout.ts +268 -0
- package/src/components/timeline-annotation-markers.tsx +185 -0
- package/src/components/timeline-annotation-reveal-context.ts +9 -0
- package/src/components/timeline-annotation-selection.tsx +106 -54
- package/src/components/timeline-annotation-shared.ts +245 -0
- package/src/components/timeline-annotations-dialog.tsx +185 -128
- package/src/components/timeline-annotations.tsx +189 -45
- package/src/components/timeline-reading-progress.ts +80 -0
- package/src/components/timeline-search.tsx +168 -0
- package/src/components/timeline-settlement.ts +60 -0
- package/src/components/tip-follow.ts +52 -0
- package/src/components/tool-action-review.tsx +650 -0
- package/src/components/tool-review-history.tsx +88 -0
- package/src/components/tooltip.tsx +38 -5
- package/src/components/usage-limit-notice.tsx +179 -0
- package/src/components/usage-member-list.tsx +792 -0
- package/src/components/usage-meter.tsx +382 -0
- package/src/components/use-viewer-menu-dismiss.ts +33 -0
- package/src/components/user-message-body.tsx +27 -2
- package/src/components/workspace-dock.tsx +18 -1
- package/src/composer-messages.ts +150 -0
- package/src/composer-voice-events.ts +12 -0
- package/src/composer.ts +6 -1
- package/src/connect-account-identity.tsx +34 -0
- package/src/connect-accounts.tsx +233 -0
- package/src/connect-chooser.tsx +336 -0
- package/src/connect-panel.tsx +99 -0
- package/src/connect-setup.tsx +355 -0
- package/src/connect.ts +60 -0
- package/src/connection-assets.d.ts +4 -0
- package/src/connection-catalog.tsx +162 -0
- package/src/connection-discovery.tsx +238 -0
- package/src/connection-installed.tsx +51 -0
- package/src/connection-logo.tsx +37 -0
- package/src/connection-service-logo.tsx +55 -0
- package/src/connection-service-presentation.ts +46 -0
- package/src/connection-skeleton.tsx +16 -0
- package/src/connection-type-picker.tsx +36 -0
- package/src/conversation-timeline.ts +92 -0
- package/src/desktop.ts +24 -0
- package/src/device-authorization.tsx +98 -0
- package/src/diffs.ts +16 -0
- package/src/discovery-cache.ts +42 -0
- package/src/editor.ts +13 -0
- package/src/hooks/internal.ts +3 -2
- package/src/hooks/latest-question.ts +164 -0
- package/src/hooks/shared-machines-list.ts +34 -1
- package/src/hooks/shared-query.ts +721 -0
- package/src/hooks/use-attached-browsers.ts +1 -1
- package/src/hooks/use-auth-runs.ts +0 -27
- package/src/hooks/use-available-models.ts +21 -15
- package/src/hooks/use-browser-frame-stream.ts +19 -2
- package/src/hooks/use-browser-session.ts +680 -205
- package/src/hooks/use-browser-sessions.ts +32 -1
- package/src/hooks/use-client-config-flags.ts +118 -0
- package/src/hooks/use-codex-accounts.ts +247 -47
- package/src/hooks/use-composer.ts +276 -47
- package/src/hooks/use-computer-frame-stream.ts +25 -7
- package/src/hooks/use-computer-input-posture.ts +94 -0
- package/src/hooks/use-computer-session.ts +409 -95
- package/src/hooks/use-connect.ts +23 -0
- package/src/hooks/use-desktop-stream.ts +19 -28
- package/src/hooks/use-file-attachments.ts +80 -4
- package/src/hooks/use-goal.ts +107 -13
- package/src/hooks/use-last-started-turn-policy.ts +46 -14
- package/src/hooks/use-machines.ts +4 -0
- package/src/hooks/use-new-conversation.ts +374 -0
- package/src/hooks/use-new-session-draft.ts +813 -0
- package/src/hooks/use-realtime-voice-models.ts +46 -0
- package/src/hooks/use-sandbox-files.ts +68 -20
- package/src/hooks/use-session-background-commands.ts +22 -11
- package/src/hooks/use-session-capabilities.ts +3 -2
- package/src/hooks/use-session-conversation.ts +150 -0
- package/src/hooks/use-session-events.ts +703 -382
- package/src/hooks/use-session-lineage.ts +20 -16
- package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
- package/src/hooks/use-session-model-account.ts +174 -0
- package/src/hooks/use-session.ts +53 -21
- package/src/hooks/use-slash-commands.ts +6 -11
- package/src/hooks/use-supergrok-accounts.ts +22 -8
- package/src/hooks/use-terminal-stream.ts +153 -27
- package/src/hooks/use-transcription.ts +6 -0
- package/src/hooks/use-turn-queue.ts +2 -1
- package/src/hooks/use-usage.ts +100 -0
- package/src/hooks/use-vaults.ts +153 -0
- package/src/hooks/use-video-artifact-playback.ts +1 -1
- package/src/hooks/use-voice-input.ts +127 -19
- package/src/hooks/use-workspace-capture.ts +3 -2
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/hooks/use-workspace-sessions.ts +97 -32
- package/src/human-input-model.ts +359 -0
- package/src/identity-link-accounts.tsx +148 -0
- package/src/identity-link-consent.tsx +192 -0
- package/src/index.ts +262 -16
- package/src/interaction.ts +1 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/error-message.ts +37 -0
- package/src/lib/format.ts +213 -16
- package/src/lib/host-theme.ts +529 -0
- package/src/lib/interaction-errors.ts +15 -0
- package/src/lib/menu-styles.ts +44 -0
- package/src/lib/notify-observer.ts +15 -0
- package/src/lib/pierre-diffs-loader.ts +53 -0
- package/src/lib/provider-recovery.ts +243 -0
- package/src/lib/scrollbars.ts +30 -0
- package/src/lib/terminal-input-readiness.ts +51 -0
- package/src/lib/transient-retry.ts +99 -0
- package/src/lib/workbench-peers.ts +62 -0
- package/src/lib/xterm-theme.ts +23 -21
- package/src/mcp-connection-status.ts +35 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +299 -74
- package/src/native-previews.ts +100 -0
- package/src/new-session-draft.ts +22 -0
- package/src/notification-text.ts +81 -0
- package/src/older-history.ts +22 -4
- package/src/organization-model.ts +118 -0
- package/src/plugin-details.tsx +185 -0
- package/src/plugin-discovery.tsx +196 -0
- package/src/preview-loading-paint.ts +68 -0
- package/src/provider.tsx +74 -15
- package/src/queue-presentation-model.ts +464 -0
- package/src/realtime/dropdown-menu.tsx +19 -12
- package/src/realtime/embedded-new-voice.tsx +1 -0
- package/src/realtime/embedded-voice.tsx +3 -0
- package/src/realtime/realtime-control.tsx +138 -501
- package/src/realtime/session-realtime.ts +566 -0
- package/src/sandbox-label-model.ts +53 -0
- package/src/scrollbars.ts +3 -0
- package/src/secret-reference-label.ts +43 -0
- package/src/secrets-ui.tsx +59 -0
- package/src/session-agents-model.ts +33 -0
- package/src/session-attention-model.ts +62 -0
- package/src/session-auth-recommendation.ts +48 -0
- package/src/session-capability-policy.ts +196 -0
- package/src/session-context.ts +3 -4
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +434 -0
- package/src/session-realtime.ts +18 -0
- package/src/session-status-model.ts +99 -0
- package/src/session-ui.ts +98 -1
- package/src/session.ts +17 -1
- package/src/site-snapshot.ts +74 -0
- package/src/sites-ui.tsx +348 -0
- package/src/sites.ts +3 -0
- package/src/skill-discovery.tsx +208 -0
- package/src/terminal.ts +26 -0
- package/src/testing/session-scenarios.ts +404 -0
- package/src/timeline/activity-rail.tsx +242 -101
- package/src/timeline/agent-identity.tsx +84 -0
- package/src/timeline/agent-row.tsx +231 -0
- package/src/timeline/allowance-exhausted-row.tsx +86 -0
- package/src/timeline/compaction-copy.ts +115 -0
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/focus-sequence.ts +44 -0
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +143 -0
- package/src/timeline/index.ts +30 -2
- package/src/timeline/knowledge-receipt.tsx +100 -0
- package/src/timeline/message-sender.tsx +71 -0
- package/src/timeline/notice-presentation.ts +48 -0
- package/src/timeline/parsers.ts +42 -2
- package/src/timeline/patch-apply-command.tsx +135 -0
- package/src/timeline/platform-activity-presentation.ts +166 -0
- package/src/timeline/platform-activity-row.tsx +15 -90
- package/src/timeline/presented-image.ts +63 -0
- package/src/timeline/projection.ts +1746 -76
- package/src/timeline/provider-logo.tsx +53 -0
- package/src/timeline/question-nav-model.ts +28 -0
- package/src/timeline/registry.ts +26 -4
- package/src/timeline/retained-image.ts +97 -0
- package/src/timeline/retained-loaders.ts +91 -0
- package/src/timeline/rolling-activity.tsx +80 -0
- package/src/timeline/screenshot-lightbox.tsx +228 -5
- package/src/timeline/session-usage-limit-row.tsx +42 -0
- package/src/timeline/shared.tsx +42 -15
- package/src/timeline/startup-dispatch-details.tsx +36 -0
- package/src/timeline/startup-preference.ts +33 -0
- package/src/timeline/startup-timings.tsx +145 -0
- package/src/timeline/tool-display-name.ts +19 -7
- package/src/timeline/tool-presentation.ts +1316 -0
- package/src/timeline/tool-renderers.tsx +835 -1138
- package/src/timeline/turn-summary-model.ts +343 -0
- package/src/timeline/turn-summary.tsx +272 -365
- package/src/timeline/types.ts +189 -6
- package/src/timeline/work-presentation.ts +189 -0
- package/src/timeline-model.ts +143 -0
- package/src/usage/allowance-copy.ts +64 -0
- package/src/usage/session-usage-limit.ts +266 -0
- package/src/usage/summary.ts +93 -0
- package/src/usage.ts +70 -0
- package/src/voice-recording-owner.ts +22 -0
- package/styles/compiled.css +2319 -748
- package/styles/connect.css +290 -0
- package/styles/connect.d.ts +1 -0
- package/styles/effective-tokens.css +1 -0
- package/styles/index.css +309 -9
- package/styles/responsive.css +99 -7
- package/styles/session-capability.css +44 -0
- package/styles/tokens.css +134 -66
- package/dist/browser-viewer-AEFHUYZK.js +0 -14
- package/dist/chunk-4MDLEHXL.js.map +0 -1
- package/dist/chunk-5YK2ZDIG.js.map +0 -1
- package/dist/chunk-D3UIAJKY.js.map +0 -1
- package/dist/chunk-ELIYWBZR.js.map +0 -1
- package/dist/chunk-H5YGYNXS.js.map +0 -1
- package/dist/chunk-HQVCKCLG.js +0 -9458
- package/dist/chunk-HQVCKCLG.js.map +0 -1
- package/dist/chunk-HUIKIDM5.js.map +0 -1
- package/dist/chunk-ICA6R6TW.js.map +0 -1
- package/dist/chunk-JVYQRRC4.js.map +0 -1
- package/dist/chunk-KCGXPP5V.js.map +0 -1
- package/dist/chunk-KZ73RL76.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js +0 -141
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-MZ2G2XBA.js.map +0 -1
- package/dist/chunk-NEXLLTP4.js.map +0 -1
- package/dist/chunk-OAM4WAAM.js.map +0 -1
- package/dist/chunk-PEI6RANI.js.map +0 -1
- package/dist/chunk-RGALCTWO.js.map +0 -1
- package/dist/chunk-W6NFJXSA.js +0 -454
- package/dist/chunk-W6NFJXSA.js.map +0 -1
- package/dist/chunk-WSK7G5LE.js +0 -170
- package/dist/chunk-WSK7G5LE.js.map +0 -1
- package/dist/chunk-X4A66EWZ.js +0 -223
- package/dist/chunk-X4A66EWZ.js.map +0 -1
- package/dist/chunk-XXWH2JLC.js.map +0 -1
- package/dist/chunk-YFKJ7VZH.js +0 -268
- package/dist/chunk-YFKJ7VZH.js.map +0 -1
- package/dist/chunk-YJFGAFBD.js.map +0 -1
- package/dist/computer-viewer-FWEZISW4.js +0 -16
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- package/dist/hooks/use-environments.d.ts +0 -41
- package/dist/hooks/use-packs.d.ts +0 -32
- package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +0 -1
- package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
- package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
- package/src/hooks/use-environments.ts +0 -182
- package/src/hooks/use-packs.ts +0 -157
- /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-ZWR6MWJ7.js.map} +0 -0
- /package/dist/{computer-viewer-FWEZISW4.js.map → chat-interactive-block-BO2IWXVK.js.map} +0 -0
|
@@ -1,16 +1,52 @@
|
|
|
1
|
+
import { QUESTION_NAV_MARGIN_PX, questionNavTarget } from "../timeline/question-nav-model";
|
|
2
|
+
import { timelineGroupIndexAtSequence } from "../timeline/focus-sequence";
|
|
3
|
+
import { RollingActivity } from "../timeline/rolling-activity";
|
|
4
|
+
import {
|
|
5
|
+
clusterIsSettled,
|
|
6
|
+
compactedLandmarkCount,
|
|
7
|
+
durationBetween,
|
|
8
|
+
flattenActivityItems,
|
|
9
|
+
isPreparingWork,
|
|
10
|
+
readableWorkDefaultOpen,
|
|
11
|
+
readableWorkHasDetails,
|
|
12
|
+
readableWorkShowsPreview,
|
|
13
|
+
readableWorkStatus,
|
|
14
|
+
} from "../timeline/work-presentation";
|
|
15
|
+
import { useErrorMessage } from "../lib/error-message";
|
|
16
|
+
import { formatElapsed } from "../timeline/turn-summary";
|
|
17
|
+
import { ActivityNoteTextContext } from "../timeline/activity-rail";
|
|
18
|
+
import { timelineGroupContainsPresentedImage } from "../timeline/presented-image";
|
|
19
|
+
import {
|
|
20
|
+
isTimelineSearchTarget,
|
|
21
|
+
TimelineSearchRevealContext,
|
|
22
|
+
useTimelineSearchNavigation,
|
|
23
|
+
type TimelineSearchTarget,
|
|
24
|
+
} from "./timeline-search";
|
|
25
|
+
import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
|
|
26
|
+
import { ChildSessionLink } from "./child-session-link";
|
|
27
|
+
import { useStartupDetails } from "../timeline/startup-preference";
|
|
28
|
+
import {
|
|
29
|
+
compactionFailureDetails,
|
|
30
|
+
compactionSkipSubtitle,
|
|
31
|
+
compactionSkipTitle,
|
|
32
|
+
} from "../timeline/compaction-copy";
|
|
1
33
|
import type {
|
|
2
34
|
DraftTimelineAnnotation,
|
|
3
35
|
MediaGenerationResult,
|
|
4
36
|
SessionEvent,
|
|
5
37
|
SessionStatus,
|
|
6
38
|
} from "@opengeni/sdk";
|
|
39
|
+
import { dequal } from "dequal/lite";
|
|
7
40
|
import {
|
|
8
41
|
ArrowDownIcon,
|
|
9
42
|
ArrowRightIcon,
|
|
43
|
+
ArrowUpIcon,
|
|
10
44
|
BotIcon,
|
|
11
45
|
CheckCircle2Icon,
|
|
12
46
|
CheckIcon,
|
|
47
|
+
ChevronDownIcon,
|
|
13
48
|
ChevronRightIcon,
|
|
49
|
+
CornerDownRightIcon,
|
|
14
50
|
PauseCircleIcon,
|
|
15
51
|
PauseIcon,
|
|
16
52
|
MessageCircleQuestionIcon,
|
|
@@ -23,16 +59,19 @@ import {
|
|
|
23
59
|
TriangleAlertIcon,
|
|
24
60
|
XCircleIcon,
|
|
25
61
|
} from "lucide-react";
|
|
26
|
-
import type { ComponentType } from "react";
|
|
27
|
-
import { AnimatePresence, motion } from "motion/react";
|
|
28
|
-
import { Collapsible } from "radix-ui";
|
|
62
|
+
import type { ComponentType, CSSProperties } from "react";
|
|
63
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
29
64
|
import {
|
|
30
65
|
Component,
|
|
66
|
+
Fragment,
|
|
31
67
|
Suspense,
|
|
68
|
+
createContext,
|
|
32
69
|
lazy,
|
|
33
70
|
memo,
|
|
34
71
|
useCallback,
|
|
72
|
+
useContext,
|
|
35
73
|
useEffect,
|
|
74
|
+
useId,
|
|
36
75
|
useLayoutEffect,
|
|
37
76
|
useMemo,
|
|
38
77
|
useRef,
|
|
@@ -40,6 +79,11 @@ import {
|
|
|
40
79
|
type ReactNode,
|
|
41
80
|
} from "react";
|
|
42
81
|
import { cn } from "../lib/cn";
|
|
82
|
+
import {
|
|
83
|
+
MessageSenderContext,
|
|
84
|
+
MessageSenderSlot,
|
|
85
|
+
type RenderMessageSender,
|
|
86
|
+
} from "../timeline/message-sender";
|
|
43
87
|
import { formatClockTime, formatRelativeTime, truncate } from "../lib/format";
|
|
44
88
|
import { prefersReducedMotion } from "../lib/motion";
|
|
45
89
|
import {
|
|
@@ -47,22 +91,39 @@ import {
|
|
|
47
91
|
type OlderHistoryLoadReceipt,
|
|
48
92
|
type OlderHistoryLoader,
|
|
49
93
|
} from "../older-history";
|
|
50
|
-
import { Markdown } from "./markdown";
|
|
94
|
+
import { Markdown, type MarkdownProps } from "./markdown";
|
|
95
|
+
import { linkifyText } from "./linkified-text";
|
|
96
|
+
import { OpenGeniLinkProvider, type OpenGeniLinkResolver } from "./open-geni-links";
|
|
97
|
+
import {
|
|
98
|
+
TimelineBeforeLayout,
|
|
99
|
+
captureTimelineAnchor,
|
|
100
|
+
timelineAnchorCorrection,
|
|
101
|
+
timelineHasReader,
|
|
102
|
+
type TimelineAnchor,
|
|
103
|
+
} from "./timeline-anchor";
|
|
104
|
+
import { useReadingProgress } from "./timeline-reading-progress";
|
|
105
|
+
import {
|
|
106
|
+
animateTimelineSettlement,
|
|
107
|
+
captureTimelineSettlement,
|
|
108
|
+
type TimelineSettlement,
|
|
109
|
+
} from "./timeline-settlement";
|
|
51
110
|
import {
|
|
52
111
|
UserMessageBody,
|
|
53
112
|
UserMessageDisclosureProvider,
|
|
54
113
|
type UserMessageDisclosureContextValue,
|
|
114
|
+
type UserMessageDisclosureLabels,
|
|
55
115
|
} from "./user-message-body";
|
|
56
116
|
import {
|
|
57
117
|
createTipFollowState,
|
|
58
118
|
readerScrollUpPx,
|
|
59
119
|
tipFollowCancel,
|
|
60
|
-
tipFollowCompensateShrink,
|
|
61
120
|
tipFollowCompensateViewportShrink,
|
|
121
|
+
tipFollowObserveContentShrink,
|
|
62
122
|
tipFollowStep,
|
|
63
123
|
supportsScrollEndEvent,
|
|
64
124
|
TIP_FOLLOW_READER_UP_EPS_PX,
|
|
65
125
|
TIP_FOLLOW_SHRINK_EPS_PX,
|
|
126
|
+
type TipFollowContentShrinkBaseline,
|
|
66
127
|
type TipFollowState,
|
|
67
128
|
} from "./tip-follow";
|
|
68
129
|
import {
|
|
@@ -87,6 +148,7 @@ import {
|
|
|
87
148
|
type VideoArtifactPlaybackLoader,
|
|
88
149
|
type ToolRegistry,
|
|
89
150
|
type TurnSummaryOptions,
|
|
151
|
+
type TurnSummaryStatus,
|
|
90
152
|
type UserMessageItem,
|
|
91
153
|
type FoldRestingState,
|
|
92
154
|
type WorkerCompletionItem,
|
|
@@ -97,42 +159,104 @@ import {
|
|
|
97
159
|
useTurnSettleOpen,
|
|
98
160
|
} from "../timeline";
|
|
99
161
|
import { CopyHoverFrame } from "./copy-button";
|
|
162
|
+
import { SecretAnswerChip } from "./secret-ref-chip";
|
|
163
|
+
import { secretAnswerLabel } from "../human-input-model";
|
|
164
|
+
import { TimelineAnnotationSourceRootContext } from "./timeline-annotation-reveal-context";
|
|
100
165
|
import { GeneratedVideoPlayer } from "./generated-video-player";
|
|
101
166
|
import {
|
|
102
167
|
MACHINE_INPUT_META,
|
|
168
|
+
agentMemberSessionId,
|
|
169
|
+
agentMemberText,
|
|
170
|
+
agentMemberTitle,
|
|
103
171
|
cleanMachineInputSummary,
|
|
172
|
+
isAgentMember,
|
|
104
173
|
machineInputBatchLabel,
|
|
105
174
|
machineInputSummaryIsUseful,
|
|
106
175
|
readableMachineInputSource,
|
|
107
176
|
} from "./machine-input-display";
|
|
177
|
+
import { AgentRow, AgentRowSection, AgentRowText } from "../timeline/agent-row";
|
|
178
|
+
import {
|
|
179
|
+
AgentIdentityProvider,
|
|
180
|
+
useAgentIdentity,
|
|
181
|
+
type ResolveSessionTitle,
|
|
182
|
+
} from "../timeline/agent-identity";
|
|
183
|
+
import { namedAgentTitle, type AgentTitleParts } from "../timeline/platform-activity-presentation";
|
|
108
184
|
import { SESSION_STATUS_META, StatusDot } from "./session-status";
|
|
185
|
+
import {
|
|
186
|
+
noticeDisplayText,
|
|
187
|
+
noticeIsResolvedApproval,
|
|
188
|
+
noticeTone,
|
|
189
|
+
} from "../timeline/notice-presentation";
|
|
109
190
|
import { TimelineComputeLabelProvider } from "../timeline/compute-label";
|
|
191
|
+
import { ProviderLogoProvider } from "../timeline/provider-logo";
|
|
110
192
|
import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
|
|
193
|
+
import {
|
|
194
|
+
AllowanceExhaustedRow,
|
|
195
|
+
AllowancePresentationProvider,
|
|
196
|
+
type RenderAllowanceExhausted,
|
|
197
|
+
} from "../timeline/allowance-exhausted-row";
|
|
198
|
+
import { SessionUsageLimitRow } from "../timeline/session-usage-limit-row";
|
|
199
|
+
import type { AllowanceLabels } from "../usage/allowance-copy";
|
|
111
200
|
import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
|
|
112
|
-
import {
|
|
201
|
+
import { TimelineAnnotationCards } from "./timeline-annotations";
|
|
113
202
|
import { TooltipProvider } from "./tooltip";
|
|
114
203
|
|
|
115
204
|
const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-selection"));
|
|
205
|
+
const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
|
|
116
206
|
|
|
117
207
|
export type MessageTimelineProps = {
|
|
208
|
+
/** Exact durable search hit; clearing it removes highlighting without moving the reader. */
|
|
209
|
+
searchTarget?: TimelineSearchTarget | null | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* A durable event sequence to open on once (an inbox item's or notification's
|
|
212
|
+
* moment): the timeline lands on the row that holds it, or the nearest row
|
|
213
|
+
* before it, instead of the latest message. Load the window around it first.
|
|
214
|
+
*/
|
|
215
|
+
focusSequence?: number | null | undefined;
|
|
216
|
+
/** Localized user-message disclosure actions, including custom UserMessageBody renderers. */
|
|
217
|
+
userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
|
|
118
218
|
/** Raw session events (projected internally) … */
|
|
119
219
|
events?: SessionEvent[] | undefined;
|
|
120
220
|
/** … or pre-projected items (e.g. from `useSessionEvents().timeline`). */
|
|
121
221
|
items?: TimelineItem[] | undefined;
|
|
122
222
|
/** Current session status (reserved; tip "Working…" chrome removed for now). */
|
|
123
223
|
status?: SessionStatus | null | undefined;
|
|
224
|
+
/** Host-owned controls beside Copy and the timestamp on settled message rows. */
|
|
225
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Who sent a person's message, drawn above its bubble. Called only for
|
|
228
|
+
* messages that carry a sender; return null for the viewer's own messages
|
|
229
|
+
* or when the host has no identity to show. Omit to show no senders.
|
|
230
|
+
*/
|
|
231
|
+
renderMessageSender?: RenderMessageSender | undefined;
|
|
124
232
|
/** Plug a markdown renderer for message bodies (e.g. streamdown). */
|
|
125
233
|
renderMessageText?:
|
|
126
|
-
| ((
|
|
234
|
+
| ((
|
|
235
|
+
text: string,
|
|
236
|
+
item: AgentMessageItem | UserMessageItem,
|
|
237
|
+
context: { searchTarget: TimelineSearchTarget | null },
|
|
238
|
+
) => ReactNode)
|
|
127
239
|
| undefined;
|
|
240
|
+
/**
|
|
241
|
+
* Render assistant `opengeni-html` / `opengeni-site` fences in the default
|
|
242
|
+
* message renderer (`SessionConversation` supplies the inline Site preview).
|
|
243
|
+
*/
|
|
244
|
+
renderInteractiveBlock?: MarkdownProps["renderInteractiveBlock"];
|
|
128
245
|
/** Drill into a spawned worker session. */
|
|
129
246
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
247
|
+
/**
|
|
248
|
+
* Name the agents this session spawns, messages, and hears from. Return the
|
|
249
|
+
* current display title for a session id, or null when unknown; the
|
|
250
|
+
* timeline falls back to the title given at spawn, then to generic copy.
|
|
251
|
+
* Keep the function identity stable across renders.
|
|
252
|
+
*/
|
|
253
|
+
resolveSessionTitle?: ResolveSessionTitle | undefined;
|
|
130
254
|
/**
|
|
131
255
|
* Deep-link a memory row (a `memory.saved` / `memory.corrected` step) to its
|
|
132
256
|
* record in the host's memory pane. Opt-in, exactly like `onReconnect`: the
|
|
133
257
|
* library draws no "View in memory" affordance without a handler — the memory
|
|
134
258
|
* row is then non-interactive rich content. This is the switch that makes the
|
|
135
|
-
* deep-link a first-party
|
|
259
|
+
* deep-link a first-party Opengeni capability without other SDK consumers
|
|
136
260
|
* opting into it. The app supplies it (it owns routing to the memory pane).
|
|
137
261
|
*/
|
|
138
262
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
@@ -144,6 +268,17 @@ export type MessageTimelineProps = {
|
|
|
144
268
|
* a Reconnect button without a handler to run it.
|
|
145
269
|
*/
|
|
146
270
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
271
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
272
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
273
|
+
/**
|
|
274
|
+
* Replace the "usage limit reached" row shown when a workspace or member
|
|
275
|
+
* usage allowance stops work (an `allowance_exhausted` refusal). Receives the
|
|
276
|
+
* typed refusal (scope, reset time) and the default row; return undefined to
|
|
277
|
+
* keep the default. Use it to point people at your own plan or admin page.
|
|
278
|
+
*/
|
|
279
|
+
renderAllowanceExhausted?: RenderAllowanceExhausted | undefined;
|
|
280
|
+
/** Replace the words of the default "usage limit reached" row. */
|
|
281
|
+
allowanceExhaustedLabels?: Partial<AllowanceLabels> | undefined;
|
|
147
282
|
/**
|
|
148
283
|
* Decide which durable authentication notices this timeline presents.
|
|
149
284
|
* Defaults to showing every notice. Embedded hosts can suppress notices for
|
|
@@ -164,6 +299,16 @@ export type MessageTimelineProps = {
|
|
|
164
299
|
* `createDefaultToolRegistry({ entries })` to add custom tool renderers.
|
|
165
300
|
*/
|
|
166
301
|
toolRegistry?: ToolRegistry | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Open Opengeni object links the agent writes in replies and progress notes:
|
|
304
|
+
* `artifact:<file>`, `sandbox:<path>`, editable artifacts, and Sites. Return
|
|
305
|
+
* a host URL (`{ href }`) or action (`{ open }`); unhandled targets render as
|
|
306
|
+
* unavailable text instead of a console link that 404s inside the host.
|
|
307
|
+
* Applies to the default message renderer and to any `Markdown` a custom
|
|
308
|
+
* `renderMessageText` renders. `SessionConversation` supplies file and
|
|
309
|
+
* sandbox downloads by default.
|
|
310
|
+
*/
|
|
311
|
+
resolveLink?: OpenGeniLinkResolver | undefined;
|
|
167
312
|
/** Resolve opaque retained screenshot receipts through the authenticated host SDK. */
|
|
168
313
|
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
169
314
|
/** Resolve permanent workspace image/file receipts through the authenticated host SDK. */
|
|
@@ -176,11 +321,22 @@ export type MessageTimelineProps = {
|
|
|
176
321
|
*/
|
|
177
322
|
computeLabel?: string | null | undefined;
|
|
178
323
|
/** Customize collapsed turn facets for this timeline instance. */
|
|
324
|
+
genieLoading?: GenieLoadingOptions | undefined;
|
|
179
325
|
turnSummary?: TurnSummaryOptions | undefined;
|
|
180
326
|
/** Follow new events when pinned to the bottom. Defaults to true. */
|
|
181
327
|
autoFollow?: boolean | undefined;
|
|
182
328
|
/** Capture a same-row text selection into the host's canonical composer draft. */
|
|
183
329
|
onAnnotate?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
|
|
330
|
+
/**
|
|
331
|
+
* Hide the floating "Back to your message" pill while the timeline viewport
|
|
332
|
+
* is shorter than this (px), e.g. a narrow embed above a decision card.
|
|
333
|
+
* Defaults to 0 (always available).
|
|
334
|
+
*/
|
|
335
|
+
questionNavMinViewportHeight?: number | undefined;
|
|
336
|
+
/** Composer draft quotes currently attached to the next send. */
|
|
337
|
+
draftAnnotations?: readonly DraftTimelineAnnotation[] | undefined;
|
|
338
|
+
/** Open the composer review list for one numbered draft badge. */
|
|
339
|
+
onDraftAnnotationSelect?: ((id: string) => void) | undefined;
|
|
184
340
|
/** Older durable history exists above the current window (see useSessionEvents). */
|
|
185
341
|
hasOlder?: boolean | undefined;
|
|
186
342
|
/** An older window is being fetched; shows the quiet top shimmer. */
|
|
@@ -199,13 +355,16 @@ export type MessageTimelineProps = {
|
|
|
199
355
|
hasNewer?: boolean | undefined;
|
|
200
356
|
/** A newer history page is being fetched. */
|
|
201
357
|
loadingNewer?: boolean | undefined;
|
|
202
|
-
/** Page forward through history
|
|
203
|
-
onLoadNewer?: (() =>
|
|
358
|
+
/** Page forward through history. Return the request promise to enable inline error/retry. */
|
|
359
|
+
onLoadNewer?: (() => unknown) | undefined;
|
|
204
360
|
/**
|
|
205
361
|
* Reload the live tip window. When omitted, Jump to latest only re-pins and
|
|
206
362
|
* scrolls the in-memory window.
|
|
207
363
|
*/
|
|
208
364
|
onJumpToLatest?: (() => void | Promise<void>) | undefined;
|
|
365
|
+
/** @deprecated Retained for source compatibility; contextual prompt navigation
|
|
366
|
+
* uses only mounted prompts and never invokes this global resolver. */
|
|
367
|
+
onJumpToLatestQuestion?: (() => Promise<number | null>) | undefined;
|
|
209
368
|
/** Host-owned content appended after timeline groups, such as startup progress. */
|
|
210
369
|
trailingState?: ReactNode | undefined;
|
|
211
370
|
emptyState?: ReactNode | undefined;
|
|
@@ -244,6 +403,24 @@ const PIN_THRESHOLD_PX = 48;
|
|
|
244
403
|
* line-sized streaming movement.
|
|
245
404
|
*/
|
|
246
405
|
const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
|
|
406
|
+
/**
|
|
407
|
+
* Floating timeline navigation: two identical small round arrow buttons at fixed
|
|
408
|
+
* spots, centered on the conversation. Back to your message floats 12px below
|
|
409
|
+
* the top edge, or just below an expanded work-header strip while that strip is
|
|
410
|
+
* pinned (32px, 44px on coarse pointers) so the strip stays a full-width
|
|
411
|
+
* target. Jump to latest floats 12px above the bottom edge. They never dodge
|
|
412
|
+
* the content beneath them (floating over a sliver of text is fine), so they
|
|
413
|
+
* cannot wander as rows stream in or the host resizes. Coarse pointers get a
|
|
414
|
+
* larger invisible hit area instead of a larger button.
|
|
415
|
+
*/
|
|
416
|
+
const NAV_BUTTON_CLASS =
|
|
417
|
+
"pointer-events-auto relative inline-flex size-8 items-center justify-center rounded-full border border-og-border bg-og-surface-3/90 text-og-fg-muted shadow-og-md backdrop-blur hover:border-og-border-strong hover:text-og-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent pointer-coarse:before:absolute pointer-coarse:before:-inset-1.5 pointer-coarse:before:content-['']";
|
|
418
|
+
const NAV_FADE = {
|
|
419
|
+
initial: { opacity: 0 },
|
|
420
|
+
animate: { opacity: 1 },
|
|
421
|
+
exit: { opacity: 0 },
|
|
422
|
+
transition: { duration: 0.15, ease: "easeOut" },
|
|
423
|
+
} as const;
|
|
247
424
|
/**
|
|
248
425
|
* Prefetch older history when the top sentinel is this far from the viewport.
|
|
249
426
|
* After a page loads we stay cool until the reader leaves this band (scrolls
|
|
@@ -252,13 +429,21 @@ const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
|
|
|
252
429
|
const OLDER_PREFETCH_MARGIN_PX = 400;
|
|
253
430
|
const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`;
|
|
254
431
|
const PRIMARY_ACTION_CLASS =
|
|
255
|
-
"inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md bg-og-
|
|
432
|
+
"inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md border border-og-primary-border bg-og-primary text-og-primary-fg px-3 py-1.5 text-og-menu font-medium sm:w-auto";
|
|
256
433
|
const MESSAGE_BUBBLE_CLASS =
|
|
257
434
|
"w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg";
|
|
258
435
|
const WAITING_PILL_CLASS =
|
|
259
436
|
"border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting";
|
|
260
437
|
const LOADING_CHIP_CLASS =
|
|
261
438
|
"pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur";
|
|
439
|
+
/** How long live progress notes take to fold into (or out of) their work row. */
|
|
440
|
+
const LIVE_NOTE_FOLD_MS = 220;
|
|
441
|
+
/** The timeline list's `gap-5`; a folded note gives it back with `-mt-5`. */
|
|
442
|
+
const LIVE_NOTE_GAP_PX = 20;
|
|
443
|
+
/** Longest a bottom-space loan waits for an opening work list to grow into it. */
|
|
444
|
+
const LIVE_NOTE_RESERVE_MS = 700;
|
|
445
|
+
/** Lets a live work disclosure report whether it is open (see `reportLiveFold`). */
|
|
446
|
+
const LiveNoteFoldContext = createContext<((workId: string, open: boolean) => void) | null>(null);
|
|
262
447
|
|
|
263
448
|
// State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled,
|
|
264
449
|
// 3 explicit no-progress waiting to become an underfill retry.
|
|
@@ -276,13 +461,18 @@ function invokeOlderLoad(
|
|
|
276
461
|
load: OlderHistoryLoader,
|
|
277
462
|
noProgress: () => void,
|
|
278
463
|
attempt: OlderLoadAttempt,
|
|
464
|
+
preserveTail = false,
|
|
279
465
|
): 1 | undefined {
|
|
280
466
|
try {
|
|
281
467
|
// Receipt creation is captured synchronously through legacy wrappers such
|
|
282
468
|
// as `() => void loadOlder()`, even when the wrapper discards the return.
|
|
283
|
-
const result = invokeOlderHistoryLoaderWithReceiptCapture(
|
|
284
|
-
|
|
285
|
-
|
|
469
|
+
const result = invokeOlderHistoryLoaderWithReceiptCapture(
|
|
470
|
+
load,
|
|
471
|
+
(receipt) => {
|
|
472
|
+
attempt[2] = receipt;
|
|
473
|
+
},
|
|
474
|
+
preserveTail,
|
|
475
|
+
) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
|
|
286
476
|
const receipt =
|
|
287
477
|
attempt[2] ??
|
|
288
478
|
(typeof (result as { committed?: unknown } | undefined)?.committed === "boolean"
|
|
@@ -362,6 +552,49 @@ function isNearBottom(node: HTMLElement): boolean {
|
|
|
362
552
|
return gap < Math.min(PIN_THRESHOLD_PX, maxScroll);
|
|
363
553
|
}
|
|
364
554
|
|
|
555
|
+
/** Content-space top of a timeline group, or null when it is not mounted. */
|
|
556
|
+
function contentTopOf(node: HTMLElement, groupKey: string): number | null {
|
|
557
|
+
const element = node.querySelector(`[data-og-group-key="${cssEscapeAttribute(groupKey)}"]`);
|
|
558
|
+
if (!(element instanceof HTMLElement)) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
return element.getBoundingClientRect().top - node.getBoundingClientRect().top + node.scrollTop;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The question the reader is inside of, once its start has scrolled out of view. */
|
|
565
|
+
/**
|
|
566
|
+
* `belowHeader`: an expanded work header is pinned to the top of the scrollport,
|
|
567
|
+
* so the up arrow sits just below that strip instead of covering it.
|
|
568
|
+
*/
|
|
569
|
+
type QuestionNav = { key: string; belowHeader: boolean };
|
|
570
|
+
|
|
571
|
+
function readQuestionNav(node: HTMLElement): QuestionNav | null {
|
|
572
|
+
const view = node.getBoundingClientRect();
|
|
573
|
+
const prompts = [...node.querySelectorAll<HTMLElement>("[data-og-prompt]")].flatMap((prompt) => {
|
|
574
|
+
const key = prompt.dataset.ogGroupKey;
|
|
575
|
+
if (!key) return [];
|
|
576
|
+
const bounds = prompt.getBoundingClientRect();
|
|
577
|
+
return [{ key, top: bounds.top, bottom: bounds.bottom }];
|
|
578
|
+
});
|
|
579
|
+
const key = questionNavTarget(prompts, { top: view.top, height: view.height });
|
|
580
|
+
return key ? { key, belowHeader: workHeaderPinned(node, view) } : null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** True while an expanded outer work header is stuck to the scrollport's top edge. */
|
|
584
|
+
function workHeaderPinned(node: HTMLElement, view: DOMRect): boolean {
|
|
585
|
+
for (const header of node.querySelectorAll<HTMLElement>(
|
|
586
|
+
'[data-og-work-header="outer"][data-state="open"]',
|
|
587
|
+
)) {
|
|
588
|
+
const box = header.getBoundingClientRect();
|
|
589
|
+
if (Math.abs(box.top - view.top) <= 1 && box.bottom > view.top) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function sameQuestionNav(a: QuestionNav | null, b: QuestionNav | null): boolean {
|
|
595
|
+
return a === b || (!!a && !!b && a.key === b.key && a.belowHeader === b.belowHeader);
|
|
596
|
+
}
|
|
597
|
+
|
|
365
598
|
/** Escape a value for use inside a CSS attribute selector. */
|
|
366
599
|
function cssEscapeAttribute(value: string): string {
|
|
367
600
|
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
@@ -377,13 +610,24 @@ function cssEscapeAttribute(value: string): string {
|
|
|
377
610
|
* with a "jump to latest" affordance when the reader scrolls back.
|
|
378
611
|
*/
|
|
379
612
|
export function MessageTimeline({
|
|
613
|
+
resolveLink,
|
|
614
|
+
renderInteractiveBlock,
|
|
615
|
+
userMessageDisclosureLabels,
|
|
616
|
+
searchTarget,
|
|
617
|
+
focusSequence,
|
|
380
618
|
events,
|
|
381
619
|
items,
|
|
382
620
|
status: _status,
|
|
621
|
+
renderMessageActions,
|
|
622
|
+
renderMessageSender,
|
|
383
623
|
renderMessageText,
|
|
384
624
|
onOpenSession,
|
|
625
|
+
resolveSessionTitle,
|
|
385
626
|
onMemoryClick,
|
|
386
627
|
onReconnect,
|
|
628
|
+
renderAuthNeeded,
|
|
629
|
+
renderAllowanceExhausted,
|
|
630
|
+
allowanceExhaustedLabels,
|
|
387
631
|
shouldRenderAuthNeeded,
|
|
388
632
|
resolveProviderLogo,
|
|
389
633
|
toolRegistry = defaultToolRegistry,
|
|
@@ -391,9 +635,13 @@ export function MessageTimeline({
|
|
|
391
635
|
loadRetainedArtifact,
|
|
392
636
|
loadVideoArtifactPlayback,
|
|
393
637
|
computeLabel = null,
|
|
638
|
+
genieLoading,
|
|
394
639
|
turnSummary,
|
|
395
640
|
autoFollow = true,
|
|
396
641
|
onAnnotate,
|
|
642
|
+
questionNavMinViewportHeight = 0,
|
|
643
|
+
draftAnnotations,
|
|
644
|
+
onDraftAnnotationSelect,
|
|
397
645
|
hasOlder = false,
|
|
398
646
|
loadingOlder = false,
|
|
399
647
|
onLoadOlder,
|
|
@@ -407,6 +655,7 @@ export function MessageTimeline({
|
|
|
407
655
|
emptyState,
|
|
408
656
|
className,
|
|
409
657
|
}: MessageTimelineProps) {
|
|
658
|
+
const formatError = useErrorMessage();
|
|
410
659
|
const resolvedItems = useMemo(() => {
|
|
411
660
|
const projectedItems = items ?? buildTimeline(events ?? []);
|
|
412
661
|
if (!shouldRenderAuthNeeded) {
|
|
@@ -416,9 +665,134 @@ export function MessageTimeline({
|
|
|
416
665
|
(item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
|
|
417
666
|
);
|
|
418
667
|
}, [items, events, shouldRenderAuthNeeded]);
|
|
419
|
-
const
|
|
668
|
+
const searchItem = searchTarget
|
|
669
|
+
? resolvedItems.find((item) => isTimelineSearchTarget(item, searchTarget))
|
|
670
|
+
: undefined;
|
|
671
|
+
const searchRevealKey =
|
|
672
|
+
searchItem && searchTarget
|
|
673
|
+
? JSON.stringify([
|
|
674
|
+
searchItem.id,
|
|
675
|
+
searchTarget.query,
|
|
676
|
+
searchTarget.occurrence ?? 0,
|
|
677
|
+
searchTarget.offset ?? null,
|
|
678
|
+
])
|
|
679
|
+
: null;
|
|
680
|
+
const searchTargetRef = useRef(searchTarget);
|
|
681
|
+
searchTargetRef.current = searchTarget;
|
|
682
|
+
const renderSearchableMessageText = useMemo(
|
|
683
|
+
() =>
|
|
684
|
+
renderMessageText
|
|
685
|
+
? (text: string, item: AgentMessageItem | UserMessageItem) =>
|
|
686
|
+
renderMessageText(text, item, {
|
|
687
|
+
searchTarget: searchItem?.id === item.id ? (searchTarget ?? null) : null,
|
|
688
|
+
})
|
|
689
|
+
: (text: string, item: AgentMessageItem | UserMessageItem) => {
|
|
690
|
+
const body = (
|
|
691
|
+
<Markdown
|
|
692
|
+
searchTarget={searchItem?.id === item.id ? searchTarget : null}
|
|
693
|
+
softLineBreaks
|
|
694
|
+
streaming={item.kind === "agent-message" && item.streaming}
|
|
695
|
+
renderInteractiveBlock={
|
|
696
|
+
item.kind === "agent-message" ? renderInteractiveBlock : undefined
|
|
697
|
+
}
|
|
698
|
+
>
|
|
699
|
+
{text}
|
|
700
|
+
</Markdown>
|
|
701
|
+
);
|
|
702
|
+
return item.kind === "user-message" ? (
|
|
703
|
+
<UserMessageBody messageId={item.id} text={text}>
|
|
704
|
+
{body}
|
|
705
|
+
</UserMessageBody>
|
|
706
|
+
) : (
|
|
707
|
+
body
|
|
708
|
+
);
|
|
709
|
+
},
|
|
710
|
+
[renderInteractiveBlock, renderMessageText, searchItem?.id, searchTarget],
|
|
711
|
+
);
|
|
712
|
+
// Event-window identity is independent of projected rows (partial messages
|
|
713
|
+
// can acquire a different first-delta id when older text arrives).
|
|
714
|
+
const sourceItems = events ?? items;
|
|
420
715
|
const olderBoundaryKey = sourceItems?.[0]?.id;
|
|
421
|
-
const
|
|
716
|
+
const newerBoundaryKey = `${events?.[0]?.sessionId ?? ""}:${olderBoundaryKey ?? ""}`;
|
|
717
|
+
const newerScopeRef = useRef(newerBoundaryKey);
|
|
718
|
+
newerScopeRef.current = newerBoundaryKey;
|
|
719
|
+
const newerAttemptRef = useRef<{ pending: boolean } | null>(null);
|
|
720
|
+
const newerRetryButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
721
|
+
const [newerRetryPending, setNewerRetryPending] = useState(false);
|
|
722
|
+
const [newerFailure, setNewerFailure] = useState<{ key: string; message: string } | null>(null);
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
newerScopeRef.current = newerBoundaryKey;
|
|
725
|
+
newerAttemptRef.current = null;
|
|
726
|
+
setNewerFailure(null);
|
|
727
|
+
setNewerRetryPending(false);
|
|
728
|
+
return () => {
|
|
729
|
+
newerScopeRef.current = "";
|
|
730
|
+
newerAttemptRef.current = null;
|
|
731
|
+
};
|
|
732
|
+
}, [newerBoundaryKey]);
|
|
733
|
+
const requestNewer = useCallback(
|
|
734
|
+
(explicitRetry = false) => {
|
|
735
|
+
if (
|
|
736
|
+
newerScopeRef.current !== newerBoundaryKey ||
|
|
737
|
+
!onLoadNewer ||
|
|
738
|
+
loadingNewer ||
|
|
739
|
+
(newerAttemptRef.current && (!explicitRetry || newerAttemptRef.current.pending))
|
|
740
|
+
) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const attempt = { pending: true };
|
|
744
|
+
newerAttemptRef.current = attempt;
|
|
745
|
+
if (explicitRetry) setNewerRetryPending(true);
|
|
746
|
+
const isCurrent = () =>
|
|
747
|
+
newerAttemptRef.current === attempt && newerScopeRef.current === newerBoundaryKey;
|
|
748
|
+
// Both synchronous host errors and rejected promises belong to this
|
|
749
|
+
// boundary. Retain the failed attempt so observers cannot hot-retry it.
|
|
750
|
+
void Promise.resolve()
|
|
751
|
+
.then(() => (isCurrent() ? onLoadNewer() : undefined))
|
|
752
|
+
.then(
|
|
753
|
+
() => {
|
|
754
|
+
if (!isCurrent()) return;
|
|
755
|
+
newerAttemptRef.current = null;
|
|
756
|
+
// The successful page removes the recovery control. Return focus
|
|
757
|
+
// to the reading surface without moving the reader's viewport.
|
|
758
|
+
if (document.activeElement === newerRetryButtonRef.current) {
|
|
759
|
+
scrollRef.current?.focus({ preventScroll: true });
|
|
760
|
+
}
|
|
761
|
+
setNewerFailure(null);
|
|
762
|
+
setNewerRetryPending(false);
|
|
763
|
+
},
|
|
764
|
+
(reason: unknown) => {
|
|
765
|
+
if (!isCurrent()) return;
|
|
766
|
+
attempt.pending = false;
|
|
767
|
+
setNewerRetryPending(false);
|
|
768
|
+
setNewerFailure({
|
|
769
|
+
key: newerBoundaryKey,
|
|
770
|
+
message: formatError(reason),
|
|
771
|
+
});
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
},
|
|
775
|
+
[onLoadNewer, loadingNewer, newerBoundaryKey, formatError],
|
|
776
|
+
);
|
|
777
|
+
const previousSourceIdsRef = useRef(new Set<string>());
|
|
778
|
+
const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
|
|
779
|
+
const readingAnchorRef = useRef<TimelineAnchor | null>(null);
|
|
780
|
+
const settlementRef = useRef<TimelineSettlement | null>(null);
|
|
781
|
+
const settlementAnimationRef = useRef<Animation | null>(null);
|
|
782
|
+
const stopSettlement = useCallback(() => {
|
|
783
|
+
settlementAnimationRef.current?.cancel();
|
|
784
|
+
settlementAnimationRef.current = null;
|
|
785
|
+
}, []);
|
|
786
|
+
const olderPageBudgetRef = useRef(0);
|
|
787
|
+
const [olderDemand, setOlderDemand] = useState(0);
|
|
788
|
+
const readableTurns = turnSummary?.rolling === true;
|
|
789
|
+
const projectedGroups = useMemo(
|
|
790
|
+
() => groupTimeline(resolvedItems, { readableTurns }),
|
|
791
|
+
[resolvedItems, readableTurns],
|
|
792
|
+
);
|
|
793
|
+
// A new timeline revision (including session/turn replacement) owns layout.
|
|
794
|
+
// Never leave an old decorative transform running on its retained rows.
|
|
795
|
+
useLayoutEffect(() => stopSettlement, [projectedGroups, stopSettlement]);
|
|
422
796
|
const annotationSources = useMemo(() => {
|
|
423
797
|
const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
|
|
424
798
|
for (const item of resolvedItems) {
|
|
@@ -433,12 +807,16 @@ export function MessageTimeline({
|
|
|
433
807
|
}
|
|
434
808
|
return sources;
|
|
435
809
|
}, [resolvedItems]);
|
|
436
|
-
const groups = useStableTimelineGroupKeys(allGroups);
|
|
437
810
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
438
811
|
const topSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
439
812
|
const bottomSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
440
813
|
const previousBulkFirstKeyRef = useRef<string | null | undefined>(undefined);
|
|
441
814
|
const [pinned, setPinned] = useState(true);
|
|
815
|
+
const { groups: allGroups, release: releaseProgress } = useReadingProgress(
|
|
816
|
+
projectedGroups,
|
|
817
|
+
autoFollow && pinned && !hasNewer,
|
|
818
|
+
scrollRef.current,
|
|
819
|
+
);
|
|
442
820
|
const [canSkipTipCatchup, setCanSkipTipCatchup] = useState(false);
|
|
443
821
|
const canSkipTipCatchupRef = useRef(false);
|
|
444
822
|
const [bulkActive, setBulkActive] = useState(true);
|
|
@@ -460,6 +838,17 @@ export function MessageTimeline({
|
|
|
460
838
|
);
|
|
461
839
|
const underfillRetryReadyRef = useRef(false);
|
|
462
840
|
const resizeFollowRafRef = useRef<number | null>(null);
|
|
841
|
+
const questionNavFrameRef = useRef<number | null>(null);
|
|
842
|
+
const [questionNav, setQuestionNav] = useState<QuestionNav | null>(null);
|
|
843
|
+
const questionNavMinViewportRef = useRef(questionNavMinViewportHeight);
|
|
844
|
+
questionNavMinViewportRef.current = questionNavMinViewportHeight;
|
|
845
|
+
const scheduleQuestionNavRef = useRef<() => void>(() => {});
|
|
846
|
+
// Unpinned is not the same as away from the tip: focusing a control in the
|
|
847
|
+
// conversation (Copy, a connection card) hands the view to the reader while
|
|
848
|
+
// they are still at the bottom. Offer the jump only once there is something
|
|
849
|
+
// below them, or a newer window / catch-up to skip.
|
|
850
|
+
const [awayFromTip, setAwayFromTip] = useState(false);
|
|
851
|
+
const jumpPillShown = autoFollow && ((!pinned && awayFromTip) || hasNewer || canSkipTipCatchup);
|
|
463
852
|
const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
|
|
464
853
|
// Content stays invisible until the tip is hard-parked across a short
|
|
465
854
|
// post-commit settle (two rAFs). That absorbs sync late layout while hidden
|
|
@@ -506,6 +895,8 @@ export function MessageTimeline({
|
|
|
506
895
|
* jumps (Vimium) settle via scrollend while the camera is idle.
|
|
507
896
|
*/
|
|
508
897
|
const readerIntentArmRef = useRef(false);
|
|
898
|
+
/** Gesture-start geometry survives commits before the native scroll event. */
|
|
899
|
+
const readerIntentStartRef = useRef<{ scrollTop: number; maxScroll: number } | null>(null);
|
|
509
900
|
/**
|
|
510
901
|
* Count of camera/snap scrollTop writes whose scroll echoes are not yet
|
|
511
902
|
* consumed. A boolean was wrong when the browser coalesced two writes into
|
|
@@ -530,10 +921,195 @@ export function MessageTimeline({
|
|
|
530
921
|
// deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
|
|
531
922
|
// that already settled closed — or that the reader closed — never reopens.
|
|
532
923
|
const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
|
|
924
|
+
// Live work disclosures that are open. While open, that turn's progress
|
|
925
|
+
// notes read inside the disclosure, so their copies above it fold away.
|
|
926
|
+
const [openLiveFolds, setOpenLiveFolds] = useState<ReadonlySet<string>>(() => new Set());
|
|
927
|
+
const openLiveFoldsRef = useRef<ReadonlySet<string>>(openLiveFolds);
|
|
928
|
+
const liveNoteIdsByWorkRef = useRef<ReadonlyMap<string, readonly string[]>>(new Map());
|
|
929
|
+
const liveFoldMotionRef = useRef<{ frame: number | null; finish: () => void } | null>(null);
|
|
930
|
+
// The bottom-space loan currently on the scroller, by owner.
|
|
931
|
+
const liveFoldReserveRef = useRef<object | null>(null);
|
|
932
|
+
// writeScrollTop is declared further down; the fold reaches it through this.
|
|
933
|
+
const writeScrollTopRef = useRef<(node: HTMLElement, top: number) => void>(() => {});
|
|
934
|
+
const reportLiveFold = useCallback((workId: string, open: boolean) => {
|
|
935
|
+
const current = openLiveFoldsRef.current;
|
|
936
|
+
if (current.has(workId) === open) return;
|
|
937
|
+
const next = new Set(current);
|
|
938
|
+
if (open) next.add(workId);
|
|
939
|
+
else next.delete(workId);
|
|
940
|
+
openLiveFoldsRef.current = next;
|
|
941
|
+
liveFoldMotionRef.current?.finish();
|
|
942
|
+
const scroller = scrollRef.current;
|
|
943
|
+
const rows = (liveNoteIdsByWorkRef.current.get(workId) ?? []).flatMap((id) => {
|
|
944
|
+
const row = scroller?.querySelector<HTMLElement>(
|
|
945
|
+
`[data-og-live-note-id="${cssEscapeAttribute(id)}"]`,
|
|
946
|
+
);
|
|
947
|
+
return row ? [row] : [];
|
|
948
|
+
});
|
|
949
|
+
if (!scroller || rows.length === 0) {
|
|
950
|
+
setOpenLiveFolds(next);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const header = scroller.querySelector<HTMLElement>(
|
|
954
|
+
`[data-og-work-id="${cssEscapeAttribute(workId)}"] [data-og-work-header="outer"]`,
|
|
955
|
+
);
|
|
956
|
+
const viewTop = scroller.getBoundingClientRect().top;
|
|
957
|
+
const headerTop = header?.getBoundingClientRect().top;
|
|
958
|
+
if (
|
|
959
|
+
!open &&
|
|
960
|
+
header &&
|
|
961
|
+
headerTop !== undefined &&
|
|
962
|
+
(headerTop < viewTop || headerTop > viewTop + scroller.clientHeight)
|
|
963
|
+
) {
|
|
964
|
+
// Closed from the row's sticky header after reading down the list: the
|
|
965
|
+
// row is no longer where the reader is. Like any big collapse, land on
|
|
966
|
+
// it in one step — the notes return above and the closed row rests at
|
|
967
|
+
// the bottom edge — rather than animating content the reader can't see.
|
|
968
|
+
setOpenLiveFolds(next);
|
|
969
|
+
requestFrame(() =>
|
|
970
|
+
requestFrame(() => {
|
|
971
|
+
const row = header.closest<HTMLElement>("[data-og-timeline-group-anchor]");
|
|
972
|
+
if (!row?.isConnected) return;
|
|
973
|
+
const box = row.getBoundingClientRect();
|
|
974
|
+
const top = scroller.getBoundingClientRect().top;
|
|
975
|
+
const bottom = top + scroller.clientHeight;
|
|
976
|
+
// + the scroller's pb-6, as at the true end.
|
|
977
|
+
if (box.bottom > bottom || box.top < top)
|
|
978
|
+
writeScrollTopRef.current(scroller, scroller.scrollTop + box.bottom - bottom + 24);
|
|
979
|
+
scheduleQuestionNavRef.current();
|
|
980
|
+
}),
|
|
981
|
+
);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
// Opening a live row near the bottom drops its one-line preview while its
|
|
985
|
+
// list grows from nothing; closing shrinks the list before the notes have
|
|
986
|
+
// grown back. For that moment the page is shorter than where the reader
|
|
987
|
+
// is, so the browser would clamp the scroll and everything would drop.
|
|
988
|
+
// This runs before that frame paints: keep the reader's position and lend
|
|
989
|
+
// one screen of space at the bottom (the shortfall keeps changing through
|
|
990
|
+
// the commit) until the content has grown back into it.
|
|
991
|
+
const readerTop = lastScrollTopRef.current;
|
|
992
|
+
const reserve = readerTop > 0.5 ? scroller.clientHeight : 0;
|
|
993
|
+
const loan = {};
|
|
994
|
+
if (reserve > 0) {
|
|
995
|
+
liveFoldReserveRef.current = loan;
|
|
996
|
+
scroller.style.paddingBottom = `calc(1.5rem + ${reserve}px)`;
|
|
997
|
+
if (Math.abs(scroller.scrollTop - readerTop) > 0.5)
|
|
998
|
+
writeScrollTopRef.current(scroller, readerTop);
|
|
999
|
+
}
|
|
1000
|
+
const releaseReserve = (deadline: number) => {
|
|
1001
|
+
// Only this motion's loan; a newer toggle may have replaced it.
|
|
1002
|
+
if (reserve === 0 || liveFoldReserveRef.current !== loan) return;
|
|
1003
|
+
const needed = scroller.scrollTop + scroller.clientHeight - (scroller.scrollHeight - reserve);
|
|
1004
|
+
if (needed > 0.5 && performance.now() < deadline && scroller.isConnected) {
|
|
1005
|
+
requestFrame(() => releaseReserve(deadline));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
liveFoldReserveRef.current = null;
|
|
1009
|
+
scroller.style.removeProperty("padding-bottom");
|
|
1010
|
+
};
|
|
1011
|
+
// The notes fold into (or out of) the work row in one short motion. The
|
|
1012
|
+
// row glides to where it should rest: opening, into the first note's place
|
|
1013
|
+
// (or the top edge, when that note was above view) so its list starts just
|
|
1014
|
+
// beneath; closing, down below its returning notes but never past the
|
|
1015
|
+
// bottom edge. When everything is in view that is exactly what happens on
|
|
1016
|
+
// its own; otherwise the scroll is written in the same frame as the
|
|
1017
|
+
// heights, so it cannot lag. Native scroll anchoring is suspended for the
|
|
1018
|
+
// motion; inline styles pin the current look across the state commit, end
|
|
1019
|
+
// exactly at the classes' resting geometry, and are then removed.
|
|
1020
|
+
const workRow = header?.closest<HTMLElement>("[data-og-timeline-group-anchor]") ?? null;
|
|
1021
|
+
const rowStart = workRow?.getBoundingClientRect().top;
|
|
1022
|
+
const viewBottom = viewTop + scroller.clientHeight;
|
|
1023
|
+
let landing =
|
|
1024
|
+
open && rowStart !== undefined
|
|
1025
|
+
? Math.max(rows[0]!.getBoundingClientRect().top, viewTop)
|
|
1026
|
+
: null;
|
|
1027
|
+
const heights = rows.map((row) => (open ? row.offsetHeight : 0));
|
|
1028
|
+
const apply = (progress: number) => {
|
|
1029
|
+
const shown = open ? 1 - progress : progress;
|
|
1030
|
+
rows.forEach((note, index) => {
|
|
1031
|
+
// The list gap travels inside the row as padding (margin cancels it),
|
|
1032
|
+
// so a shrinking row never overlaps the one above it mid-motion.
|
|
1033
|
+
note.style.marginTop = `${-LIVE_NOTE_GAP_PX}px`;
|
|
1034
|
+
note.style.paddingTop = `${LIVE_NOTE_GAP_PX * shown}px`;
|
|
1035
|
+
note.style.height = `${(heights[index]! + LIVE_NOTE_GAP_PX) * shown}px`;
|
|
1036
|
+
note.style.opacity = String(shown);
|
|
1037
|
+
});
|
|
1038
|
+
if (workRow && rowStart !== undefined && landing !== null) {
|
|
1039
|
+
const drift =
|
|
1040
|
+
workRow.getBoundingClientRect().top - (rowStart + (landing - rowStart) * progress);
|
|
1041
|
+
if (Math.abs(drift) > 0.5) writeScrollTopRef.current(scroller, scroller.scrollTop + drift);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
for (const row of rows) {
|
|
1045
|
+
row.style.boxSizing = "border-box";
|
|
1046
|
+
row.style.overflow = "hidden";
|
|
1047
|
+
row.style.overflowAnchor = "none";
|
|
1048
|
+
}
|
|
1049
|
+
scroller.style.overflowAnchor = "none";
|
|
1050
|
+
apply(0);
|
|
1051
|
+
const fold: { frame: number | null; finish: () => void } = {
|
|
1052
|
+
frame: null,
|
|
1053
|
+
finish: () => {
|
|
1054
|
+
if (fold.frame != null) cancelFrame(fold.frame);
|
|
1055
|
+
for (const row of rows) {
|
|
1056
|
+
for (const property of [
|
|
1057
|
+
"height",
|
|
1058
|
+
"margin-top",
|
|
1059
|
+
"padding-top",
|
|
1060
|
+
"opacity",
|
|
1061
|
+
"overflow",
|
|
1062
|
+
"overflow-anchor",
|
|
1063
|
+
"box-sizing",
|
|
1064
|
+
])
|
|
1065
|
+
row.style.removeProperty(property);
|
|
1066
|
+
}
|
|
1067
|
+
scroller.style.removeProperty("overflow-anchor");
|
|
1068
|
+
releaseReserve(performance.now() + LIVE_NOTE_RESERVE_MS);
|
|
1069
|
+
// The row may now be pinned to the top without any scroll: re-place
|
|
1070
|
+
// the up arrow (it sits below a pinned work header).
|
|
1071
|
+
scheduleQuestionNavRef.current();
|
|
1072
|
+
if (liveFoldMotionRef.current === fold) liveFoldMotionRef.current = null;
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
liveFoldMotionRef.current = fold;
|
|
1076
|
+
const duration = prefersReducedMotion() ? 0 : LIVE_NOTE_FOLD_MS;
|
|
1077
|
+
let began: number | null = null;
|
|
1078
|
+
const step = (now: number) => {
|
|
1079
|
+
fold.frame = null;
|
|
1080
|
+
if (!rows.every((row) => row.isConnected)) return fold.finish();
|
|
1081
|
+
if (!open && began === null) {
|
|
1082
|
+
// Unfolding: the natural heights are known only once the folded
|
|
1083
|
+
// class is gone; the inline styles still hold them at zero.
|
|
1084
|
+
rows.forEach((note, index) => (heights[index] = note.scrollHeight));
|
|
1085
|
+
if (workRow && rowStart !== undefined) {
|
|
1086
|
+
const returning = heights.reduce((sum, height) => sum + height + LIVE_NOTE_GAP_PX, 0);
|
|
1087
|
+
// The closed row is its header plus one preview line (its list is
|
|
1088
|
+
// still collapsing now, so the row's own height is not final yet).
|
|
1089
|
+
const closedRow = 2 * (header?.getBoundingClientRect().height ?? 0);
|
|
1090
|
+
const lowest = viewBottom - closedRow - 24;
|
|
1091
|
+
landing = Math.min(rowStart + returning, Math.max(lowest, rowStart));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
began ??= now;
|
|
1095
|
+
const linear = duration === 0 ? 1 : Math.min(1, (now - began) / duration);
|
|
1096
|
+
apply(1 - (1 - linear) ** 3);
|
|
1097
|
+
if (linear < 1) fold.frame = requestFrame(step);
|
|
1098
|
+
else fold.finish();
|
|
1099
|
+
};
|
|
1100
|
+
fold.frame = requestFrame(step);
|
|
1101
|
+
setOpenLiveFolds(next);
|
|
1102
|
+
}, []);
|
|
1103
|
+
useEffect(
|
|
1104
|
+
() => () => {
|
|
1105
|
+
liveFoldMotionRef.current?.finish();
|
|
1106
|
+
},
|
|
1107
|
+
[],
|
|
1108
|
+
);
|
|
533
1109
|
const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
|
|
534
1110
|
const seenActivityIdsRef = useRef<Set<string>>(new Set());
|
|
535
1111
|
const firstItemGroupKeyRef = useRef<string | null>(null);
|
|
536
|
-
const
|
|
1112
|
+
const firstItemGroupOffsetTopRef = useRef<number | null>(null);
|
|
537
1113
|
const firstItemContentTopRef = useRef<number | null>(null);
|
|
538
1114
|
const firstItemId = resolvedItems[0]?.id ?? null;
|
|
539
1115
|
// Pagination ownership follows the oldest committed input, before host
|
|
@@ -553,6 +1129,35 @@ export function MessageTimeline({
|
|
|
553
1129
|
previousBulkFirstKeyRef.current !== undefined &&
|
|
554
1130
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
555
1131
|
const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
1132
|
+
const groups = useStableTimelineGroupKeys(allGroups, !bulkRender);
|
|
1133
|
+
// Each live progress note → whether its turn's work disclosure is open.
|
|
1134
|
+
const [liveNoteFolds, liveNoteIdsByWork] = useMemo(() => {
|
|
1135
|
+
const folds = new Map<string, boolean>();
|
|
1136
|
+
const byWork = new Map<string, readonly string[]>();
|
|
1137
|
+
for (const { group } of groups) {
|
|
1138
|
+
if (group.kind !== "activity" || !group.work?.liveNoteIds) continue;
|
|
1139
|
+
byWork.set(group.id, group.work.liveNoteIds);
|
|
1140
|
+
for (const id of group.work.liveNoteIds) folds.set(id, openLiveFolds.has(group.id));
|
|
1141
|
+
}
|
|
1142
|
+
return [folds, byWork] as const;
|
|
1143
|
+
}, [groups, openLiveFolds]);
|
|
1144
|
+
useLayoutEffect(() => {
|
|
1145
|
+
liveNoteIdsByWorkRef.current = liveNoteIdsByWork;
|
|
1146
|
+
}, [liveNoteIdsByWork]);
|
|
1147
|
+
const turnsWithOutput = useMemo(
|
|
1148
|
+
() =>
|
|
1149
|
+
new Set(
|
|
1150
|
+
resolvedItems.flatMap((item) =>
|
|
1151
|
+
"turnId" in item &&
|
|
1152
|
+
item.turnId &&
|
|
1153
|
+
(item.kind === "tool-call" ||
|
|
1154
|
+
((item.kind === "agent-message" || item.kind === "reasoning") && item.text.trim()))
|
|
1155
|
+
? [item.turnId]
|
|
1156
|
+
: [],
|
|
1157
|
+
),
|
|
1158
|
+
),
|
|
1159
|
+
[resolvedItems],
|
|
1160
|
+
);
|
|
556
1161
|
|
|
557
1162
|
const applyCanSkipTipCatchup = useCallback((value: boolean) => {
|
|
558
1163
|
if (canSkipTipCatchupRef.current !== value) {
|
|
@@ -592,6 +1197,7 @@ export function MessageTimeline({
|
|
|
592
1197
|
// Pure tip-follow camera. Pin intent uses clamp conservation, not timers.
|
|
593
1198
|
const followRef = useRef<TipFollowState>(createTipFollowState());
|
|
594
1199
|
const followFrameRef = useRef<number | null>(null);
|
|
1200
|
+
const contentShrinkBaselineRef = useRef<TipFollowContentShrinkBaseline | null>(null);
|
|
595
1201
|
|
|
596
1202
|
const syncScrollBaseline = useCallback((node: HTMLElement) => {
|
|
597
1203
|
lastScrollTopRef.current = node.scrollTop;
|
|
@@ -606,6 +1212,9 @@ export function MessageTimeline({
|
|
|
606
1212
|
if (before === next) {
|
|
607
1213
|
return;
|
|
608
1214
|
}
|
|
1215
|
+
// Unpinned camera/anchor writes invalidate return intent. A pinned
|
|
1216
|
+
// pointer gesture still needs its cumulative leave budget across writes.
|
|
1217
|
+
if (!pinnedRef.current || !readerIntentArmRef.current) readerIntentStartRef.current = null;
|
|
609
1218
|
programmaticScrollRef.current += 1;
|
|
610
1219
|
node.scrollTop = next;
|
|
611
1220
|
if (node.scrollTop === before) {
|
|
@@ -615,6 +1224,7 @@ export function MessageTimeline({
|
|
|
615
1224
|
programmaticScrollRef.current -= 1;
|
|
616
1225
|
}
|
|
617
1226
|
}, []);
|
|
1227
|
+
writeScrollTopRef.current = writeScrollTop;
|
|
618
1228
|
|
|
619
1229
|
const cancelLeaveFallback = useCallback(() => {
|
|
620
1230
|
if (leaveFallbackRafRef.current != null) {
|
|
@@ -628,7 +1238,13 @@ export function MessageTimeline({
|
|
|
628
1238
|
cancelLeaveFallback();
|
|
629
1239
|
}, [cancelLeaveFallback]);
|
|
630
1240
|
|
|
1241
|
+
const clearReaderIntent = useCallback(() => {
|
|
1242
|
+
readerIntentArmRef.current = false;
|
|
1243
|
+
readerIntentStartRef.current = null;
|
|
1244
|
+
}, []);
|
|
1245
|
+
|
|
631
1246
|
const stopFollow = useCallback(() => {
|
|
1247
|
+
contentShrinkBaselineRef.current = null;
|
|
632
1248
|
followRef.current = tipFollowCancel(followRef.current);
|
|
633
1249
|
if (followFrameRef.current != null) {
|
|
634
1250
|
cancelFrame(followFrameRef.current);
|
|
@@ -637,29 +1253,43 @@ export function MessageTimeline({
|
|
|
637
1253
|
}, []);
|
|
638
1254
|
|
|
639
1255
|
/** Reader left the tip — wheel, keyboard, pointer-armed scroll-up, or scrollend. */
|
|
640
|
-
const releasePinFromReader = useCallback(
|
|
641
|
-
(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1256
|
+
const releasePinFromReader = useCallback(() => {
|
|
1257
|
+
if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
clearReaderIntent();
|
|
1261
|
+
clearPendingReaderLeave();
|
|
1262
|
+
stopFollow();
|
|
1263
|
+
applyPinned(false);
|
|
1264
|
+
if (wantPinRef.current) {
|
|
1265
|
+
wantPinRef.current = false;
|
|
1266
|
+
}
|
|
1267
|
+
if (!olderPrefetchArmedRef.current) {
|
|
1268
|
+
olderPrefetchArmedRef.current = true;
|
|
1269
|
+
setOlderPrefetchArmed(true);
|
|
1270
|
+
}
|
|
1271
|
+
}, [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow]);
|
|
1272
|
+
|
|
1273
|
+
useEffect(() => {
|
|
1274
|
+
if (!readableTurns) return;
|
|
1275
|
+
const node = scrollRef.current;
|
|
1276
|
+
if (!node) return;
|
|
1277
|
+
const onSelection = () => {
|
|
1278
|
+
const selection = node.ownerDocument.getSelection();
|
|
1279
|
+
if (
|
|
1280
|
+
selection &&
|
|
1281
|
+
!selection.isCollapsed &&
|
|
1282
|
+
node.contains(selection.anchorNode) &&
|
|
1283
|
+
node.contains(selection.focusNode)
|
|
1284
|
+
) {
|
|
1285
|
+
stopSettlement();
|
|
1286
|
+
releasePinFromReader();
|
|
1287
|
+
disclosureKeepsUnpinnedRef.current = true;
|
|
659
1288
|
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
1289
|
+
};
|
|
1290
|
+
node.ownerDocument.addEventListener("selectionchange", onSelection);
|
|
1291
|
+
return () => node.ownerDocument.removeEventListener("selectionchange", onSelection);
|
|
1292
|
+
}, [readableTurns, releasePinFromReader, stopSettlement]);
|
|
663
1293
|
|
|
664
1294
|
/**
|
|
665
1295
|
* Settled away from the tip while the camera is idle — Vimium / unfocused
|
|
@@ -680,7 +1310,7 @@ export function MessageTimeline({
|
|
|
680
1310
|
clearPendingReaderLeave();
|
|
681
1311
|
return;
|
|
682
1312
|
}
|
|
683
|
-
releasePinFromReader(
|
|
1313
|
+
releasePinFromReader();
|
|
684
1314
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
685
1315
|
},
|
|
686
1316
|
[autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
|
|
@@ -701,6 +1331,22 @@ export function MessageTimeline({
|
|
|
701
1331
|
});
|
|
702
1332
|
}, [cancelLeaveFallback, releasePinAfterScrollSettled]);
|
|
703
1333
|
|
|
1334
|
+
const requestEarlierFromReader = () => {
|
|
1335
|
+
// Upward navigation replaces any earlier downward gesture, even when
|
|
1336
|
+
// already unpinned and the engine never delivered scrollend.
|
|
1337
|
+
clearReaderIntent();
|
|
1338
|
+
releasePinFromReader();
|
|
1339
|
+
wantPinRef.current = false;
|
|
1340
|
+
// A stationary upward gesture is still demand. Successful short/folded
|
|
1341
|
+
// pages may never create enough range to leave the prefetch band.
|
|
1342
|
+
olderPageBudgetRef.current = 8;
|
|
1343
|
+
if (olderLoadAttemptRef.current?.[1] === 2) {
|
|
1344
|
+
olderLoadAttemptRef.current = null;
|
|
1345
|
+
setOlderDemand((value) => value + 1);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const touchPositionRef = useRef<{ x: number; y: number } | null>(null);
|
|
1349
|
+
|
|
704
1350
|
const onWheel = (event: {
|
|
705
1351
|
deltaY: number;
|
|
706
1352
|
deltaX: number;
|
|
@@ -717,13 +1363,17 @@ export function MessageTimeline({
|
|
|
717
1363
|
if (wheelConsumedByNestedScrollable(event)) {
|
|
718
1364
|
return;
|
|
719
1365
|
}
|
|
1366
|
+
stopSettlement();
|
|
720
1367
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1368
|
+
programmaticScrollRef.current = 0;
|
|
721
1369
|
if (event.deltaY >= 0) {
|
|
1370
|
+
const node = scrollRef.current;
|
|
1371
|
+
readerIntentStartRef.current = node
|
|
1372
|
+
? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
|
|
1373
|
+
: null;
|
|
722
1374
|
return;
|
|
723
1375
|
}
|
|
724
|
-
|
|
725
|
-
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
726
|
-
releasePinFromReader(node);
|
|
1376
|
+
requestEarlierFromReader();
|
|
727
1377
|
};
|
|
728
1378
|
|
|
729
1379
|
/** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
|
|
@@ -731,11 +1381,13 @@ export function MessageTimeline({
|
|
|
731
1381
|
button: number;
|
|
732
1382
|
pointerType: string;
|
|
733
1383
|
target: EventTarget | null;
|
|
1384
|
+
currentTarget: EventTarget | null;
|
|
734
1385
|
}) => {
|
|
735
1386
|
// Primary button / touch / pen only. Ignore right-click etc.
|
|
736
1387
|
if (event.button && event.pointerType === "mouse") {
|
|
737
1388
|
return;
|
|
738
1389
|
}
|
|
1390
|
+
stopSettlement();
|
|
739
1391
|
// Clicks on chips/buttons/links must not arm — their settle collapse
|
|
740
1392
|
// also drops scrollTop and would false-unpin. Drag on prose/scroller may.
|
|
741
1393
|
if (
|
|
@@ -745,7 +1397,13 @@ export function MessageTimeline({
|
|
|
745
1397
|
return;
|
|
746
1398
|
}
|
|
747
1399
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1400
|
+
programmaticScrollRef.current = 0;
|
|
1401
|
+
const node =
|
|
1402
|
+
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
748
1403
|
readerIntentArmRef.current = true;
|
|
1404
|
+
readerIntentStartRef.current = node
|
|
1405
|
+
? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
|
|
1406
|
+
: null;
|
|
749
1407
|
};
|
|
750
1408
|
|
|
751
1409
|
const onKeyDown = (event: { key: string; currentTarget: EventTarget | null }) => {
|
|
@@ -757,18 +1415,25 @@ export function MessageTimeline({
|
|
|
757
1415
|
event.key === "Home" ||
|
|
758
1416
|
event.key === "End"
|
|
759
1417
|
) {
|
|
1418
|
+
stopSettlement();
|
|
760
1419
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1420
|
+
// Reader navigation supersedes a pending camera/anchor scroll echo,
|
|
1421
|
+
// including downward keys that do not explicitly release the pin.
|
|
1422
|
+
programmaticScrollRef.current = 0;
|
|
1423
|
+
const node = scrollRef.current;
|
|
1424
|
+
readerIntentStartRef.current = node
|
|
1425
|
+
? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
|
|
1426
|
+
: null;
|
|
761
1427
|
}
|
|
762
1428
|
if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
|
|
763
1429
|
return;
|
|
764
1430
|
}
|
|
765
|
-
|
|
766
|
-
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
767
|
-
releasePinFromReader(node);
|
|
1431
|
+
requestEarlierFromReader();
|
|
768
1432
|
};
|
|
769
1433
|
|
|
770
1434
|
const snapToBottom = useCallback(
|
|
771
1435
|
(node: HTMLElement) => {
|
|
1436
|
+
clearReaderIntent();
|
|
772
1437
|
stopFollow();
|
|
773
1438
|
cancelLeaveFallback();
|
|
774
1439
|
writeScrollTop(node, Math.max(0, node.scrollHeight - node.clientHeight));
|
|
@@ -781,7 +1446,14 @@ export function MessageTimeline({
|
|
|
781
1446
|
};
|
|
782
1447
|
applyCanSkipTipCatchup(false);
|
|
783
1448
|
},
|
|
784
|
-
[
|
|
1449
|
+
[
|
|
1450
|
+
applyCanSkipTipCatchup,
|
|
1451
|
+
cancelLeaveFallback,
|
|
1452
|
+
clearReaderIntent,
|
|
1453
|
+
stopFollow,
|
|
1454
|
+
syncScrollBaseline,
|
|
1455
|
+
writeScrollTop,
|
|
1456
|
+
],
|
|
785
1457
|
);
|
|
786
1458
|
|
|
787
1459
|
const beginUserMessageDisclosureChange = useCallback(
|
|
@@ -839,8 +1511,65 @@ export function MessageTimeline({
|
|
|
839
1511
|
() => ({
|
|
840
1512
|
expandedByMessageId: userMessageDisclosureMemoryRef.current,
|
|
841
1513
|
beginChange: beginUserMessageDisclosureChange,
|
|
1514
|
+
labels: {
|
|
1515
|
+
showMore: userMessageDisclosureLabels?.showMore,
|
|
1516
|
+
showLess: userMessageDisclosureLabels?.showLess,
|
|
1517
|
+
},
|
|
1518
|
+
}),
|
|
1519
|
+
[
|
|
1520
|
+
beginUserMessageDisclosureChange,
|
|
1521
|
+
userMessageDisclosureLabels?.showMore,
|
|
1522
|
+
userMessageDisclosureLabels?.showLess,
|
|
1523
|
+
],
|
|
1524
|
+
);
|
|
1525
|
+
const timelineGroupEntryContext = useMemo<TimelineGroupEntryContext>(
|
|
1526
|
+
() => ({
|
|
1527
|
+
userMessageDisclosureContext,
|
|
1528
|
+
behavior: {
|
|
1529
|
+
renderMessageActions,
|
|
1530
|
+
renderMessageText: renderSearchableMessageText,
|
|
1531
|
+
onOpenSession,
|
|
1532
|
+
onMemoryClick,
|
|
1533
|
+
onReconnect,
|
|
1534
|
+
renderAuthNeeded,
|
|
1535
|
+
resolveProviderLogo,
|
|
1536
|
+
toolRegistry,
|
|
1537
|
+
loadRetainedScreenshot,
|
|
1538
|
+
loadRetainedArtifact,
|
|
1539
|
+
loadVideoArtifactPlayback,
|
|
1540
|
+
genieLoading,
|
|
1541
|
+
turnSummary,
|
|
1542
|
+
},
|
|
842
1543
|
}),
|
|
843
|
-
[
|
|
1544
|
+
[
|
|
1545
|
+
loadRetainedArtifact,
|
|
1546
|
+
loadRetainedScreenshot,
|
|
1547
|
+
loadVideoArtifactPlayback,
|
|
1548
|
+
onMemoryClick,
|
|
1549
|
+
onOpenSession,
|
|
1550
|
+
onReconnect,
|
|
1551
|
+
renderAuthNeeded,
|
|
1552
|
+
renderMessageActions,
|
|
1553
|
+
renderSearchableMessageText,
|
|
1554
|
+
resolveProviderLogo,
|
|
1555
|
+
toolRegistry,
|
|
1556
|
+
genieLoading,
|
|
1557
|
+
turnSummary,
|
|
1558
|
+
userMessageDisclosureContext,
|
|
1559
|
+
],
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1562
|
+
const releaseSearchPin = useCallback(() => {
|
|
1563
|
+
applyPinned(false);
|
|
1564
|
+
stopFollow();
|
|
1565
|
+
setRevealed(true);
|
|
1566
|
+
}, [applyPinned, stopFollow]);
|
|
1567
|
+
useTimelineSearchNavigation(
|
|
1568
|
+
scrollRef,
|
|
1569
|
+
searchItem?.id,
|
|
1570
|
+
searchTarget,
|
|
1571
|
+
releaseSearchPin,
|
|
1572
|
+
resolvedItems,
|
|
844
1573
|
);
|
|
845
1574
|
|
|
846
1575
|
const requestOlderIfUnderfilled = useCallback(
|
|
@@ -892,10 +1621,78 @@ export function MessageTimeline({
|
|
|
892
1621
|
// exact `false` is the first-party request-not-accepted receipt.
|
|
893
1622
|
// All other fulfillment retains this exact owner until its prepend
|
|
894
1623
|
// boundary commits; promise settlement alone cannot prove progress.
|
|
895
|
-
invokeOlderLoad(onLoadOlder, noProgress, attempt);
|
|
1624
|
+
invokeOlderLoad(onLoadOlder, noProgress, attempt, !retry);
|
|
896
1625
|
},
|
|
897
1626
|
[hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
|
|
898
1627
|
);
|
|
1628
|
+
const scheduleQuestionNav = useCallback(() => {
|
|
1629
|
+
if (!readableTurns || questionNavFrameRef.current != null) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
questionNavFrameRef.current = requestFrame(() => {
|
|
1633
|
+
questionNavFrameRef.current = null;
|
|
1634
|
+
const node = scrollRef.current;
|
|
1635
|
+
// A short viewport (a narrow embed above a decision card) has no room
|
|
1636
|
+
// for a floating pill that would cover the very rows being read.
|
|
1637
|
+
// clientHeight <= 1 is pre-layout/headless, not a short viewport.
|
|
1638
|
+
const roomy =
|
|
1639
|
+
node !== null &&
|
|
1640
|
+
(node.clientHeight <= 1 || node.clientHeight >= questionNavMinViewportRef.current);
|
|
1641
|
+
const next = node && roomy ? readQuestionNav(node) : null;
|
|
1642
|
+
setQuestionNav((current) => (sameQuestionNav(current, next) ? current : next));
|
|
1643
|
+
});
|
|
1644
|
+
}, [readableTurns]);
|
|
1645
|
+
scheduleQuestionNavRef.current = scheduleQuestionNav;
|
|
1646
|
+
useEffect(
|
|
1647
|
+
() => () => {
|
|
1648
|
+
if (questionNavFrameRef.current != null) {
|
|
1649
|
+
cancelFrame(questionNavFrameRef.current);
|
|
1650
|
+
questionNavFrameRef.current = null;
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
[],
|
|
1654
|
+
);
|
|
1655
|
+
const jumpToQuestion = (key: string) => {
|
|
1656
|
+
const node = scrollRef.current;
|
|
1657
|
+
const top = node ? contentTopOf(node, key) : null;
|
|
1658
|
+
if (!node || top === null) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
// Explicit reader navigation: leave the tip and park the question on top.
|
|
1662
|
+
releasePinFromReader();
|
|
1663
|
+
wantPinRef.current = false;
|
|
1664
|
+
disclosureKeepsUnpinnedRef.current = false;
|
|
1665
|
+
writeScrollTop(node, Math.max(0, top - QUESTION_NAV_MARGIN_PX));
|
|
1666
|
+
// Hand keyboard/accessibility focus to the actual destination. This also
|
|
1667
|
+
// gives native scroll anchoring the new question rather than an old work
|
|
1668
|
+
// row when the bounded context subsequently gains a later history page.
|
|
1669
|
+
node
|
|
1670
|
+
.querySelector<HTMLElement>(`[data-og-group-key="${cssEscapeAttribute(key)}"]`)
|
|
1671
|
+
?.focus({ preventScroll: true });
|
|
1672
|
+
syncScrollBaseline(node);
|
|
1673
|
+
scheduleQuestionNav();
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// Land once on the moment an inbox item or a notification points at.
|
|
1677
|
+
// Deliberately dependency-free: it retries after every commit until the
|
|
1678
|
+
// target row is laid out, and the ref guard makes the state update run once
|
|
1679
|
+
// per focus sequence, so it cannot chain renders.
|
|
1680
|
+
const focusedSequenceRef = useRef<number | null>(null);
|
|
1681
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- retries each commit; ref-guarded to run once
|
|
1682
|
+
useLayoutEffect(() => {
|
|
1683
|
+
if (!focusSequence || focusedSequenceRef.current === focusSequence) return;
|
|
1684
|
+
const index = timelineGroupIndexAtSequence(
|
|
1685
|
+
groups.map((entry) => entry.group),
|
|
1686
|
+
focusSequence,
|
|
1687
|
+
);
|
|
1688
|
+
const key = index >= 0 ? groups[index]?.key : undefined;
|
|
1689
|
+
const node = scrollRef.current;
|
|
1690
|
+
if (!key || !node || contentTopOf(node, key) === null) return;
|
|
1691
|
+
focusedSequenceRef.current = focusSequence;
|
|
1692
|
+
setRevealed(true);
|
|
1693
|
+
jumpToQuestion(key);
|
|
1694
|
+
});
|
|
1695
|
+
|
|
899
1696
|
const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
|
|
900
1697
|
requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
|
|
901
1698
|
);
|
|
@@ -919,19 +1716,41 @@ export function MessageTimeline({
|
|
|
919
1716
|
: typeof performance !== "undefined"
|
|
920
1717
|
? performance.now()
|
|
921
1718
|
: Date.now();
|
|
922
|
-
|
|
1719
|
+
let previousHeight = followRef.current.lastHeight;
|
|
1720
|
+
const previousObservedHeight = lastScrollHeightRef.current;
|
|
1721
|
+
if (
|
|
1722
|
+
contentShrinkBaselineRef.current &&
|
|
1723
|
+
previousObservedHeight > 0 &&
|
|
1724
|
+
node.scrollHeight > previousObservedHeight
|
|
1725
|
+
) {
|
|
1726
|
+
// A reversal ends the collapse sequence. If it remains below the held
|
|
1727
|
+
// baseline, adopt the recovered height; if it grew beyond the baseline,
|
|
1728
|
+
// leave that baseline for tipFollowStep to observe as real growth.
|
|
1729
|
+
contentShrinkBaselineRef.current = null;
|
|
1730
|
+
if (node.scrollHeight < previousHeight) {
|
|
1731
|
+
followRef.current = {
|
|
1732
|
+
...followRef.current,
|
|
1733
|
+
lastHeight: node.scrollHeight,
|
|
1734
|
+
cameraTop: null,
|
|
1735
|
+
};
|
|
1736
|
+
previousHeight = node.scrollHeight;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
const shrinkObservation = tipFollowObserveContentShrink(
|
|
1740
|
+
contentShrinkBaselineRef.current,
|
|
1741
|
+
previousHeight,
|
|
1742
|
+
lastScrollTopRef.current,
|
|
1743
|
+
node.scrollHeight,
|
|
1744
|
+
node.clientHeight,
|
|
1745
|
+
);
|
|
1746
|
+
contentShrinkBaselineRef.current = shrinkObservation.baseline;
|
|
923
1747
|
// Settle-collapse: compensate Δh from the pre-shrink baseline (browser
|
|
924
1748
|
// may already have clamped — don't double-subtract). Keep the follow rAF
|
|
925
1749
|
// alive so when collapse ends (or stream resumes) we ease instead of a
|
|
926
1750
|
// hard stop → flick. Do NOT tip-ease on the same frame as a real shrink
|
|
927
1751
|
// (that fight was the top-of-viewport flicker).
|
|
928
|
-
if (
|
|
929
|
-
let nextTop =
|
|
930
|
-
lastScrollTopRef.current,
|
|
931
|
-
previousHeight,
|
|
932
|
-
node.scrollHeight,
|
|
933
|
-
node.clientHeight,
|
|
934
|
-
);
|
|
1752
|
+
if (shrinkObservation.compensatedScrollTop !== null) {
|
|
1753
|
+
let nextTop = shrinkObservation.compensatedScrollTop;
|
|
935
1754
|
// A chrome/composer dock can land on the same frame as a settle-fold
|
|
936
1755
|
// (the "turn blocked" moment). Compensate BOTH in one write — adopting
|
|
937
1756
|
// the shrunk clientHeight below without gluing left the chrome height
|
|
@@ -967,15 +1786,6 @@ export function MessageTimeline({
|
|
|
967
1786
|
}
|
|
968
1787
|
return;
|
|
969
1788
|
}
|
|
970
|
-
// Sub-eps height noise: adopt height without moving the camera. Do NOT
|
|
971
|
-
// adopt clientHeight here — tipFollowStep (next line) owns that baseline
|
|
972
|
-
// and must still see a same-frame viewport shrink to glue it.
|
|
973
|
-
if (previousHeight > 0 && node.scrollHeight < previousHeight) {
|
|
974
|
-
followRef.current = {
|
|
975
|
-
...followRef.current,
|
|
976
|
-
lastHeight: node.scrollHeight,
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
1789
|
const result = tipFollowStep(followRef.current, {
|
|
980
1790
|
scrollTop: node.scrollTop,
|
|
981
1791
|
scrollHeight: node.scrollHeight,
|
|
@@ -1027,7 +1837,7 @@ export function MessageTimeline({
|
|
|
1027
1837
|
}
|
|
1028
1838
|
const previousFirstItemId = previousFirstItemIdRef.current;
|
|
1029
1839
|
const previousFirstItemGroupKey = firstItemGroupKeyRef.current;
|
|
1030
|
-
const
|
|
1840
|
+
const previousFirstItemGroupOffsetTop = firstItemGroupOffsetTopRef.current;
|
|
1031
1841
|
const previousItemContentTop = firstItemContentTopRef.current;
|
|
1032
1842
|
const previousScrollTop = lastScrollTopRef.current;
|
|
1033
1843
|
const previousMaxScroll = lastMaxScrollRef.current;
|
|
@@ -1035,15 +1845,28 @@ export function MessageTimeline({
|
|
|
1035
1845
|
previousMaxScroll <= 1 ||
|
|
1036
1846
|
previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
|
|
1037
1847
|
const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1848
|
+
const sourceChanged = olderBoundaryKey !== previousSourceBoundaryRef.current;
|
|
1849
|
+
const retainedSource =
|
|
1850
|
+
sourceItems?.some((item) => previousSourceIdsRef.current.has(item.id)) ?? false;
|
|
1851
|
+
const prepended = sourceChanged && retainedSource;
|
|
1852
|
+
previousSourceBoundaryRef.current = olderBoundaryKey;
|
|
1853
|
+
previousSourceIdsRef.current = new Set(sourceItems?.map((item) => item.id));
|
|
1854
|
+
const readingAnchor = readingAnchorRef.current;
|
|
1855
|
+
readingAnchorRef.current = null;
|
|
1856
|
+
const settlement = settlementRef.current;
|
|
1857
|
+
settlementRef.current = null;
|
|
1040
1858
|
const attempt = olderLoadAttemptRef.current;
|
|
1041
1859
|
const committedZeroOverlapOlderReplacement = !!(
|
|
1042
1860
|
attempt?.[2]?.committed &&
|
|
1043
|
-
|
|
1044
|
-
!
|
|
1861
|
+
sourceChanged &&
|
|
1862
|
+
!retainedSource
|
|
1045
1863
|
);
|
|
1046
1864
|
const restorePrependAnchor = () => {
|
|
1865
|
+
const correction = readingAnchor && timelineAnchorCorrection(node, readingAnchor);
|
|
1866
|
+
if (correction != null) {
|
|
1867
|
+
if (Math.abs(correction) > 1) writeScrollTop(node, node.scrollTop + correction);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1047
1870
|
// Keep the reader on the same retained rows. Prefer the exact first-item
|
|
1048
1871
|
// content coordinate (needed when a prepend merges inside one group),
|
|
1049
1872
|
// then the retained group offset, then scrollHeight as a final fallback.
|
|
@@ -1060,14 +1883,16 @@ export function MessageTimeline({
|
|
|
1060
1883
|
}
|
|
1061
1884
|
}
|
|
1062
1885
|
const anchorKey = previousFirstItemGroupKey;
|
|
1063
|
-
const previousAnchorTop =
|
|
1064
|
-
anchorKey != null ? previousGroupOffsetByKey.get(anchorKey) : undefined;
|
|
1065
1886
|
const anchorEl =
|
|
1066
1887
|
anchorKey != null
|
|
1067
1888
|
? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
|
|
1068
1889
|
: null;
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1890
|
+
if (
|
|
1891
|
+
delta == null &&
|
|
1892
|
+
anchorEl instanceof HTMLElement &&
|
|
1893
|
+
previousFirstItemGroupOffsetTop != null
|
|
1894
|
+
) {
|
|
1895
|
+
const moved = Math.round(anchorEl.offsetTop - previousFirstItemGroupOffsetTop);
|
|
1071
1896
|
if (moved) {
|
|
1072
1897
|
delta = moved;
|
|
1073
1898
|
}
|
|
@@ -1113,17 +1938,23 @@ export function MessageTimeline({
|
|
|
1113
1938
|
applyPinned(false);
|
|
1114
1939
|
}
|
|
1115
1940
|
} else if (prepended) {
|
|
1941
|
+
const olderAttemptState = olderLoadAttemptRef.current?.[1];
|
|
1942
|
+
const underfillOwned =
|
|
1943
|
+
!!olderLoadAttemptRef.current &&
|
|
1944
|
+
olderAttemptState !== 1 &&
|
|
1945
|
+
olderAttemptState !== 2 &&
|
|
1946
|
+
olderAttemptState !== 3;
|
|
1116
1947
|
if (
|
|
1117
1948
|
autoFollow &&
|
|
1118
1949
|
pinnedRef.current &&
|
|
1119
1950
|
!hasNewer &&
|
|
1120
1951
|
!pendingReaderLeaveRef.current &&
|
|
1121
|
-
(wasAtLiveTailBeforeCommit ||
|
|
1952
|
+
(wasAtLiveTailBeforeCommit || underfillOwned)
|
|
1122
1953
|
) {
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1954
|
+
// Still following the live tip: underfill, or a prefetch the reader
|
|
1955
|
+
// started and then returned from. Park at the new tip. A stale pin
|
|
1956
|
+
// while they are actually up in a short window (gap is not inside
|
|
1957
|
+
// PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
|
|
1127
1958
|
clearPendingReaderLeave();
|
|
1128
1959
|
snapToBottom(node);
|
|
1129
1960
|
} else {
|
|
@@ -1136,6 +1967,22 @@ export function MessageTimeline({
|
|
|
1136
1967
|
applyPinned(false);
|
|
1137
1968
|
}
|
|
1138
1969
|
}
|
|
1970
|
+
} else if (!pinnedRef.current && readingAnchor && readableTurns) {
|
|
1971
|
+
// A live work row moves behind newly streamed prose. Correct only the
|
|
1972
|
+
// residual movement native anchoring did not absorb, using the reader's
|
|
1973
|
+
// retained paragraph/control rather than the moving row's outer box.
|
|
1974
|
+
const correction = timelineAnchorCorrection(node, readingAnchor);
|
|
1975
|
+
if (correction !== null && Math.abs(correction) > 1) {
|
|
1976
|
+
writeScrollTop(node, node.scrollTop + correction);
|
|
1977
|
+
}
|
|
1978
|
+
const focused = readingAnchor.find((anchor) => anchor.focused)?.element;
|
|
1979
|
+
if (
|
|
1980
|
+
focused &&
|
|
1981
|
+
node.contains(focused) &&
|
|
1982
|
+
node.ownerDocument.activeElement === node.ownerDocument.body
|
|
1983
|
+
) {
|
|
1984
|
+
focused.focus({ preventScroll: true });
|
|
1985
|
+
}
|
|
1139
1986
|
} else if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1140
1987
|
// Load/remount (still hidden): hard-park. Live tip after reveal: ease.
|
|
1141
1988
|
// Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
|
|
@@ -1157,6 +2004,7 @@ export function MessageTimeline({
|
|
|
1157
2004
|
if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
|
|
1158
2005
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1159
2006
|
}
|
|
2007
|
+
scheduleQuestionNav();
|
|
1160
2008
|
previousFirstItemIdRef.current = firstItemId;
|
|
1161
2009
|
previousScrollHeightRef.current = node.scrollHeight;
|
|
1162
2010
|
syncScrollBaseline(node);
|
|
@@ -1170,14 +2018,12 @@ export function MessageTimeline({
|
|
|
1170
2018
|
hasNewer ||
|
|
1171
2019
|
firstItemContentTopRef.current == null;
|
|
1172
2020
|
if (needOffsets) {
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
groupOffsetByKeyRef.current = nextOffsetByKey;
|
|
2021
|
+
const committedFirstGroupKey = firstItemGroupKeyRef.current;
|
|
2022
|
+
const firstGroupEl = committedFirstGroupKey
|
|
2023
|
+
? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(committedFirstGroupKey)}"]`)
|
|
2024
|
+
: null;
|
|
2025
|
+
firstItemGroupOffsetTopRef.current =
|
|
2026
|
+
firstGroupEl instanceof HTMLElement ? firstGroupEl.offsetTop : null;
|
|
1181
2027
|
const firstItemEl = firstItemId
|
|
1182
2028
|
? node.querySelector(`[data-og-item="${cssEscapeAttribute(firstItemId)}"]`)
|
|
1183
2029
|
: null;
|
|
@@ -1189,6 +2035,8 @@ export function MessageTimeline({
|
|
|
1189
2035
|
: null;
|
|
1190
2036
|
}
|
|
1191
2037
|
|
|
2038
|
+
if (settlement) settlementAnimationRef.current = animateTimelineSettlement(settlement);
|
|
2039
|
+
|
|
1192
2040
|
// Promise settlement is not itself permission to retry. A receipt-marked
|
|
1193
2041
|
// accepted page retires its exact owner on this commit even when projection
|
|
1194
2042
|
// is empty or merges into the same first item. Without that mark, retain
|
|
@@ -1215,6 +2063,19 @@ export function MessageTimeline({
|
|
|
1215
2063
|
// Boundary progress retires the request owner. A late settlement from
|
|
1216
2064
|
// that completed prefetch cannot mutate the new window's cooldown owner.
|
|
1217
2065
|
olderLoadAttemptRef.current = [olderBoundaryKey, 2];
|
|
2066
|
+
// Bound automatic work, but let continued upward input replenish demand.
|
|
2067
|
+
// Only a committed first-party receipt proves that another page is safe.
|
|
2068
|
+
if (
|
|
2069
|
+
attempt[2]?.committed &&
|
|
2070
|
+
!pinnedRef.current &&
|
|
2071
|
+
hasOlder &&
|
|
2072
|
+
node.scrollTop <= OLDER_PREFETCH_MARGIN_PX &&
|
|
2073
|
+
olderPageBudgetRef.current > 0
|
|
2074
|
+
) {
|
|
2075
|
+
olderPageBudgetRef.current -= 1;
|
|
2076
|
+
olderLoadAttemptRef.current = null;
|
|
2077
|
+
setOlderDemand((value) => value + 1);
|
|
2078
|
+
}
|
|
1218
2079
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1219
2080
|
requestOlderIfUnderfilled(node);
|
|
1220
2081
|
});
|
|
@@ -1279,14 +2140,16 @@ export function MessageTimeline({
|
|
|
1279
2140
|
lastScrollHeightRef.current = 0;
|
|
1280
2141
|
lastClientHeightRef.current = 0;
|
|
1281
2142
|
firstItemGroupKeyRef.current = null;
|
|
1282
|
-
|
|
2143
|
+
firstItemGroupOffsetTopRef.current = null;
|
|
1283
2144
|
firstItemContentTopRef.current = null;
|
|
1284
2145
|
foldMemoryRef.current.clear();
|
|
1285
2146
|
userMessageDisclosureMemoryRef.current.clear();
|
|
1286
2147
|
disclosureKeepsUnpinnedRef.current = false;
|
|
2148
|
+
clearReaderIntent();
|
|
2149
|
+
contentShrinkBaselineRef.current = null;
|
|
1287
2150
|
seenActivityIdsRef.current.clear();
|
|
1288
2151
|
applyPinned(true);
|
|
1289
|
-
}, [allGroups.length, revealed, applyPinned]);
|
|
2152
|
+
}, [allGroups.length, revealed, applyPinned, clearReaderIntent]);
|
|
1290
2153
|
|
|
1291
2154
|
// Parent commits cover the initial/history-loading cases. Disclosure state
|
|
1292
2155
|
// changes are child-local, so the ResizeObserver below owns dynamic collapse
|
|
@@ -1383,6 +2246,7 @@ export function MessageTimeline({
|
|
|
1383
2246
|
loadingOlder,
|
|
1384
2247
|
olderBoundaryKey,
|
|
1385
2248
|
olderPrefetchArmed,
|
|
2249
|
+
olderDemand,
|
|
1386
2250
|
onLoadOlder,
|
|
1387
2251
|
requestOlderIfUnderfilled,
|
|
1388
2252
|
]);
|
|
@@ -1404,15 +2268,17 @@ export function MessageTimeline({
|
|
|
1404
2268
|
}
|
|
1405
2269
|
const observer = new IntersectionObserver(
|
|
1406
2270
|
(entries) => {
|
|
1407
|
-
|
|
1408
|
-
|
|
2271
|
+
// An underfilled history window has both sentinels visible. Advancing
|
|
2272
|
+
// it automatically would undo an explicit older-page navigation.
|
|
2273
|
+
if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
|
|
2274
|
+
requestNewer();
|
|
1409
2275
|
}
|
|
1410
2276
|
},
|
|
1411
2277
|
{ root, rootMargin: "0px 0px 1200px 0px" },
|
|
1412
2278
|
);
|
|
1413
2279
|
observer.observe(target);
|
|
1414
2280
|
return () => observer.disconnect();
|
|
1415
|
-
}, [hasNewer, loadingNewer, onLoadNewer, firstGroupKey]);
|
|
2281
|
+
}, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);
|
|
1416
2282
|
|
|
1417
2283
|
// Late layout that React commits cannot see (images decoding, fonts, code
|
|
1418
2284
|
// blocks) grows content without a commit. While pinned, soft-follow the tip;
|
|
@@ -1434,6 +2300,9 @@ export function MessageTimeline({
|
|
|
1434
2300
|
if (!current) {
|
|
1435
2301
|
return;
|
|
1436
2302
|
}
|
|
2303
|
+
// A viewport that shrank below the pill's minimum hides it now, not
|
|
2304
|
+
// on the next scroll.
|
|
2305
|
+
if (questionNavMinViewportRef.current > 0) scheduleQuestionNavRef.current();
|
|
1437
2306
|
requestOlderIfUnderfilled(current);
|
|
1438
2307
|
if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
|
|
1439
2308
|
return;
|
|
@@ -1465,6 +2334,8 @@ export function MessageTimeline({
|
|
|
1465
2334
|
// at what just became the live bottom — paging forward to the tip must not
|
|
1466
2335
|
// strand them unpinned watching new content grow below.
|
|
1467
2336
|
useEffect(() => {
|
|
2337
|
+
// Closing find is paint-only; it must not trigger this history-to-tip rule.
|
|
2338
|
+
if (searchTargetRef.current) return;
|
|
1468
2339
|
if (hasNewer) {
|
|
1469
2340
|
stopFollow();
|
|
1470
2341
|
applyPinned(false);
|
|
@@ -1487,6 +2358,15 @@ export function MessageTimeline({
|
|
|
1487
2358
|
}
|
|
1488
2359
|
}, [hasNewer, autoFollow, applyPinned, snapToBottom, stopFollow]);
|
|
1489
2360
|
|
|
2361
|
+
// After the scroll authority above has settled each commit, record whether
|
|
2362
|
+
// the reader is away from the tip (content can land below an unpinned
|
|
2363
|
+
// reader without any scroll event).
|
|
2364
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- Deliberately runs after every commit.
|
|
2365
|
+
useLayoutEffect(() => {
|
|
2366
|
+
const node = scrollRef.current;
|
|
2367
|
+
if (node && !pinnedRef.current) setAwayFromTip(!isNearBottom(node));
|
|
2368
|
+
});
|
|
2369
|
+
|
|
1490
2370
|
// Pinned: layout/camera recover tip debt; wheel/keys/pointer-arm unpin
|
|
1491
2371
|
// immediately; extension jumps settle via scrollend (or one-rAF fallback).
|
|
1492
2372
|
// Do not tip-follow-yank an in-flight unarmed scroll-away — that ate Vimium.
|
|
@@ -1495,6 +2375,9 @@ export function MessageTimeline({
|
|
|
1495
2375
|
if (!node) {
|
|
1496
2376
|
return;
|
|
1497
2377
|
}
|
|
2378
|
+
// Only an unpinned reader's distance matters for the jump.
|
|
2379
|
+
if (!pinnedRef.current) setAwayFromTip(!isNearBottom(node));
|
|
2380
|
+
scheduleQuestionNav();
|
|
1498
2381
|
const previousTop = lastScrollTopRef.current;
|
|
1499
2382
|
const previousMaxScroll = lastMaxScrollRef.current;
|
|
1500
2383
|
const nextTop = node.scrollTop;
|
|
@@ -1503,6 +2386,16 @@ export function MessageTimeline({
|
|
|
1503
2386
|
const readerUp = readerScrollUpPx(previousTop, nextTop, previousMaxScroll, nextMaxScroll);
|
|
1504
2387
|
const maxFell = nextMaxScroll < previousMaxScroll - 1;
|
|
1505
2388
|
const readerArmed = readerIntentArmRef.current;
|
|
2389
|
+
const readerIntentStart = readerIntentStartRef.current;
|
|
2390
|
+
const cumulativeReaderUp =
|
|
2391
|
+
readerArmed && readerIntentStart
|
|
2392
|
+
? readerScrollUpPx(
|
|
2393
|
+
readerIntentStart.scrollTop,
|
|
2394
|
+
nextTop,
|
|
2395
|
+
readerIntentStart.maxScroll,
|
|
2396
|
+
nextMaxScroll,
|
|
2397
|
+
)
|
|
2398
|
+
: readerUp;
|
|
1506
2399
|
const heightShrunk =
|
|
1507
2400
|
followRef.current.lastHeight > 0 &&
|
|
1508
2401
|
nextHeight < followRef.current.lastHeight - TIP_FOLLOW_SHRINK_EPS_PX;
|
|
@@ -1518,6 +2411,19 @@ export function MessageTimeline({
|
|
|
1518
2411
|
return;
|
|
1519
2412
|
}
|
|
1520
2413
|
|
|
2414
|
+
if (programmatic && !pinnedRef.current) {
|
|
2415
|
+
// Restore/camera writes while reading history are not a return to the tip.
|
|
2416
|
+
// Still expire a pending Jump-to-latest latch: the in-window jump itself
|
|
2417
|
+
// is a programmatic snap, and a later reader scroll-away can arrive
|
|
2418
|
+
// before that echo is consumed. Skipping this left the latch armed and
|
|
2419
|
+
// snapped the reader when hasNewer later flipped false.
|
|
2420
|
+
syncScrollBaseline(node);
|
|
2421
|
+
if (wantPinRef.current && !isNearBottom(node)) {
|
|
2422
|
+
wantPinRef.current = false;
|
|
2423
|
+
}
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
1521
2427
|
if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1522
2428
|
// Fold / composer / SessionChrome: viewport shrink raises maxScroll without
|
|
1523
2429
|
// growing content. Must hit tipFollow before we adopt the new clientHeight
|
|
@@ -1528,7 +2434,7 @@ export function MessageTimeline({
|
|
|
1528
2434
|
// Fold / composer content shrink: compensate before baseline sync so
|
|
1529
2435
|
// driveFollow still sees the pre-shrink scrollTop (avoid double-subtract).
|
|
1530
2436
|
if (heightShrunk || maxFell || viewportShrunk) {
|
|
1531
|
-
|
|
2437
|
+
clearReaderIntent();
|
|
1532
2438
|
clearPendingReaderLeave();
|
|
1533
2439
|
driveFollow(node);
|
|
1534
2440
|
return;
|
|
@@ -1550,9 +2456,9 @@ export function MessageTimeline({
|
|
|
1550
2456
|
clearPendingReaderLeave();
|
|
1551
2457
|
}
|
|
1552
2458
|
// Pointer-dragged scroll-up away from tip. Layout churn never arms this.
|
|
1553
|
-
if (readerArmed &&
|
|
1554
|
-
|
|
1555
|
-
|
|
2459
|
+
if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
|
|
2460
|
+
clearReaderIntent();
|
|
2461
|
+
requestEarlierFromReader();
|
|
1556
2462
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1557
2463
|
return;
|
|
1558
2464
|
}
|
|
@@ -1577,15 +2483,46 @@ export function MessageTimeline({
|
|
|
1577
2483
|
}
|
|
1578
2484
|
|
|
1579
2485
|
syncScrollBaseline(node);
|
|
2486
|
+
if (programmatic) {
|
|
2487
|
+
// Anchor restoration is never permission to resume tip-follow.
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
1580
2490
|
const nearBottom = isNearBottom(node);
|
|
1581
2491
|
|
|
1582
|
-
// Re-pin only when the reader moved toward/at the tip
|
|
1583
|
-
//
|
|
2492
|
+
// Re-pin only when the reader moved toward/at the tip. Prepend restore
|
|
2493
|
+
// and overflow-anchor raise scrollTop by roughly the same amount as
|
|
2494
|
+
// maxScroll; treating that as a scroll-down
|
|
2495
|
+
// re-pinned a compact-tail history reader (their preserved gap falls
|
|
2496
|
+
// inside PIN_THRESHOLD once the window is tall) and snapped them back.
|
|
2497
|
+
// A commit may already have adopted this scroll position into the layout
|
|
2498
|
+
// baseline. Explicit wheel/key/pointer intent retains its own start so
|
|
2499
|
+
// returning to the tip still re-pins after that interleaving.
|
|
2500
|
+
// Passive wheel handlers may run after compositor scrolling; retain the
|
|
2501
|
+
// earlier layout baseline when it still contains that reader movement.
|
|
2502
|
+
const returnStart =
|
|
2503
|
+
readerIntentStart && readerIntentStart.scrollTop <= previousTop ? readerIntentStart : null;
|
|
2504
|
+
const inserted = Math.max(0, nextMaxScroll - (returnStart?.maxScroll ?? previousMaxScroll));
|
|
2505
|
+
const towardTip = nextTop - (returnStart?.scrollTop ?? previousTop) - inserted;
|
|
2506
|
+
// Explicit intent may reach the exact live bottom despite intervening
|
|
2507
|
+
// streaming growth. Keep subtracting that growth: native anchoring alone
|
|
2508
|
+
// conserves the gap and must never qualify as reader movement.
|
|
2509
|
+
const explicitReturnToBottom = returnStart !== null && nextMaxScroll - nextTop <= 1;
|
|
1584
2510
|
const nextPinned =
|
|
1585
|
-
!hasNewer && nearBottom &&
|
|
2511
|
+
!hasNewer && nearBottom && towardTip > 0.5 && (inserted <= 1 || explicitReturnToBottom);
|
|
1586
2512
|
if (!nextPinned) {
|
|
1587
2513
|
stopFollow();
|
|
1588
2514
|
}
|
|
2515
|
+
const focused = node.ownerDocument.activeElement;
|
|
2516
|
+
if (
|
|
2517
|
+
nextPinned &&
|
|
2518
|
+
focused instanceof HTMLElement &&
|
|
2519
|
+
focused.hasAttribute("data-og-prompt") &&
|
|
2520
|
+
node.contains(focused)
|
|
2521
|
+
) {
|
|
2522
|
+
// Question navigation focuses its destination. Returning to the live
|
|
2523
|
+
// tip releases that stale reader ownership before the commit snapshot.
|
|
2524
|
+
node.focus({ preventScroll: true });
|
|
2525
|
+
}
|
|
1589
2526
|
applyPinned(nextPinned);
|
|
1590
2527
|
// A far-from-bottom scroll while a Jump-to-latest is pending is the reader
|
|
1591
2528
|
// changing their mind: drop the latch, or a stale one (host rejected or
|
|
@@ -1596,8 +2533,15 @@ export function MessageTimeline({
|
|
|
1596
2533
|
wantPinRef.current = false;
|
|
1597
2534
|
}
|
|
1598
2535
|
if (nextPinned) {
|
|
2536
|
+
clearReaderIntent();
|
|
1599
2537
|
return;
|
|
1600
2538
|
}
|
|
2539
|
+
if (readerIntentStart) {
|
|
2540
|
+
// This scroll already accounted for the gesture's movement. Retain a
|
|
2541
|
+
// fresh start for continuous input across the next commit, not stale
|
|
2542
|
+
// movement that could turn a later native clamp into a return to tip.
|
|
2543
|
+
readerIntentStartRef.current = { scrollTop: nextTop, maxScroll: nextMaxScroll };
|
|
2544
|
+
}
|
|
1601
2545
|
if (!olderPrefetchArmedRef.current) {
|
|
1602
2546
|
olderPrefetchArmedRef.current = true;
|
|
1603
2547
|
setOlderPrefetchArmed(true);
|
|
@@ -1613,6 +2557,9 @@ export function MessageTimeline({
|
|
|
1613
2557
|
return;
|
|
1614
2558
|
}
|
|
1615
2559
|
cancelLeaveFallback();
|
|
2560
|
+
// Engines can emit scrollend between tiny pointer steps. Keep the pinned
|
|
2561
|
+
// gesture's cumulative leave budget until it actually leaves the tip.
|
|
2562
|
+
if (!pinnedRef.current || !readerIntentArmRef.current) clearReaderIntent();
|
|
1616
2563
|
if (disclosureKeepsUnpinnedRef.current) {
|
|
1617
2564
|
programmaticScrollRef.current = 0;
|
|
1618
2565
|
stopFollow();
|
|
@@ -1627,308 +2574,506 @@ export function MessageTimeline({
|
|
|
1627
2574
|
}
|
|
1628
2575
|
releasePinAfterScrollSettled(node);
|
|
1629
2576
|
};
|
|
2577
|
+
// React 18 has no `onScrollEnd` prop: it warns and never calls it, while
|
|
2578
|
+
// `supportsScrollEndEvent()` still makes the camera wait for scrollend. A
|
|
2579
|
+
// native listener (re-bound each commit, so it always sees this render's
|
|
2580
|
+
// handler and scroller) works on every supported React version.
|
|
2581
|
+
useEffect(() => {
|
|
2582
|
+
const node = scrollRef.current;
|
|
2583
|
+
if (!node) return;
|
|
2584
|
+
node.addEventListener("scrollend", onScrollEnd);
|
|
2585
|
+
return () => node.removeEventListener("scrollend", onScrollEnd);
|
|
2586
|
+
});
|
|
1630
2587
|
|
|
1631
|
-
|
|
1632
|
-
<
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
data-og-timeline-scroller=""
|
|
1653
|
-
data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
|
|
1654
|
-
tabIndex={-1}
|
|
1655
|
-
onScroll={onScroll}
|
|
1656
|
-
onScrollEnd={onScrollEnd}
|
|
1657
|
-
onWheel={onWheel}
|
|
1658
|
-
onPointerDown={onPointerDown}
|
|
1659
|
-
onKeyDown={onKeyDown}
|
|
1660
|
-
style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
|
|
1661
|
-
className={cn(
|
|
1662
|
-
// tabIndex=-1 is programmatic only — never paint a focus ring on
|
|
1663
|
-
// the whole scroller (click + Shift used to flash a blue outline).
|
|
1664
|
-
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-hidden",
|
|
1665
|
-
autoFollow && pinned && !hasNewer
|
|
1666
|
-
? "[overflow-anchor:none]"
|
|
1667
|
-
: "[overflow-anchor:auto]",
|
|
1668
|
-
)}
|
|
1669
|
-
>
|
|
1670
|
-
<div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
1671
|
-
{!groups.length
|
|
1672
|
-
? (emptyState ?? (
|
|
1673
|
-
<p className="py-10 text-center text-og-menu text-og-fg-subtle">
|
|
1674
|
-
No activity yet.
|
|
1675
|
-
</p>
|
|
1676
|
-
))
|
|
1677
|
-
: null}
|
|
1678
|
-
{hasOlder && olderPrefetchArmed ? (
|
|
1679
|
-
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
1680
|
-
// must never shift content (that shift was itself a wobble).
|
|
1681
|
-
<div
|
|
1682
|
-
ref={topSentinelRef}
|
|
1683
|
-
data-og-top-sentinel=""
|
|
1684
|
-
data-og-timeline-chrome=""
|
|
1685
|
-
aria-hidden="true"
|
|
1686
|
-
className="pointer-events-none absolute inset-x-0 top-0 h-px"
|
|
1687
|
-
/>
|
|
1688
|
-
) : null}
|
|
1689
|
-
{groups.map(({ group, key }, index) => {
|
|
1690
|
-
const next = groups[index + 1]?.group;
|
|
1691
|
-
const contextCompactionCount =
|
|
1692
|
-
group.kind === "turn"
|
|
1693
|
-
? (group.contextCompactionCount ?? 0)
|
|
1694
|
-
: group.kind === "activity" &&
|
|
1695
|
-
next?.kind === "item" &&
|
|
1696
|
-
next.item.kind === "context-compaction" &&
|
|
1697
|
-
next.item.phase === "compacted"
|
|
1698
|
-
? 1
|
|
1699
|
-
: 0;
|
|
1700
|
-
return (
|
|
1701
|
-
<div key={key} data-og-timeline-group-anchor="" data-og-group-key={key}>
|
|
1702
|
-
<TimelineGroupRenderBoundary
|
|
1703
|
-
resetKeys={[
|
|
1704
|
-
group,
|
|
1705
|
-
renderMessageText,
|
|
1706
|
-
onOpenSession,
|
|
1707
|
-
onMemoryClick,
|
|
1708
|
-
onReconnect,
|
|
1709
|
-
resolveProviderLogo,
|
|
1710
|
-
toolRegistry,
|
|
1711
|
-
loadRetainedScreenshot,
|
|
1712
|
-
loadRetainedArtifact,
|
|
1713
|
-
loadVideoArtifactPlayback,
|
|
1714
|
-
turnSummary,
|
|
1715
|
-
]}
|
|
1716
|
-
>
|
|
1717
|
-
<UserMessageDisclosureProvider value={userMessageDisclosureContext}>
|
|
1718
|
-
<TimelineGroupView
|
|
1719
|
-
group={group}
|
|
1720
|
-
renderMessageText={renderMessageText}
|
|
1721
|
-
onOpenSession={onOpenSession}
|
|
1722
|
-
onMemoryClick={onMemoryClick}
|
|
1723
|
-
onReconnect={onReconnect}
|
|
1724
|
-
resolveProviderLogo={resolveProviderLogo}
|
|
1725
|
-
toolRegistry={toolRegistry}
|
|
1726
|
-
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
1727
|
-
loadRetainedArtifact={loadRetainedArtifact}
|
|
1728
|
-
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
1729
|
-
turnSummary={turnSummary}
|
|
1730
|
-
foldLiveCluster={isAgentProgress(next)}
|
|
1731
|
-
trailingAgentText={trailingAgentTextAfterTurn(group, next)}
|
|
1732
|
-
contextCompactionCount={
|
|
1733
|
-
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
1734
|
-
}
|
|
1735
|
-
/>
|
|
1736
|
-
</UserMessageDisclosureProvider>
|
|
1737
|
-
</TimelineGroupRenderBoundary>
|
|
1738
|
-
</div>
|
|
1739
|
-
);
|
|
1740
|
-
})}
|
|
1741
|
-
{groups.length > 0 && trailingState ? (
|
|
1742
|
-
<div data-og-timeline-trailing-state="">{trailingState}</div>
|
|
1743
|
-
) : null}
|
|
1744
|
-
{hasNewer ? (
|
|
1745
|
-
<div
|
|
1746
|
-
ref={bottomSentinelRef}
|
|
1747
|
-
data-og-bottom-sentinel=""
|
|
1748
|
-
data-og-timeline-chrome=""
|
|
1749
|
-
aria-hidden="true"
|
|
1750
|
-
className="h-px w-full"
|
|
1751
|
-
/>
|
|
1752
|
-
) : null}
|
|
1753
|
-
</div>
|
|
1754
|
-
</div>
|
|
1755
|
-
<AnimatePresence>
|
|
1756
|
-
{loadingOlder ||
|
|
1757
|
-
loadingOldest ||
|
|
1758
|
-
(hasOlder && onJumpToStart && olderPrefetchArmed) ||
|
|
1759
|
-
underfillRetryReady ? (
|
|
1760
|
-
// Floating over the scroller (not a timeline row) so showing and
|
|
1761
|
-
// hiding it never reflows history under the reader.
|
|
1762
|
-
<motion.div
|
|
1763
|
-
initial={{ opacity: 0, y: -6 }}
|
|
1764
|
-
animate={{ opacity: 1, y: 0 }}
|
|
1765
|
-
exit={{ opacity: 0, y: -6 }}
|
|
1766
|
-
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1767
|
-
data-og-loading-older=""
|
|
1768
|
-
aria-live="polite"
|
|
1769
|
-
className="absolute inset-x-0 top-3 z-10 flex justify-center"
|
|
2588
|
+
const timeline = (
|
|
2589
|
+
<AllowancePresentationProvider
|
|
2590
|
+
render={renderAllowanceExhausted}
|
|
2591
|
+
labels={allowanceExhaustedLabels}
|
|
2592
|
+
>
|
|
2593
|
+
<LightboxProvider>
|
|
2594
|
+
<FoldMemoryProvider value={foldMemoryRef.current}>
|
|
2595
|
+
<LiveNoteFoldContext.Provider value={reportLiveFold}>
|
|
2596
|
+
<SeenActivityIdsProvider value={seenActivityIdsRef.current}>
|
|
2597
|
+
<TimelineComputeLabelProvider value={computeLabel ?? null}>
|
|
2598
|
+
<EntranceAnimationProvider value={false}>
|
|
2599
|
+
<TooltipProvider delayDuration={400}>
|
|
2600
|
+
<TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
|
|
2601
|
+
<div
|
|
2602
|
+
className={cn("og-root relative flex min-h-0 flex-col", className)}
|
|
2603
|
+
// Sticky insets start at the scroller's padded content edge
|
|
2604
|
+
// (pt-16). Subtract exactly that padding so an expanded work
|
|
2605
|
+
// header pins flush to the scrollport. Pinning it lower left a
|
|
2606
|
+
// band of scrolling rows visible above the header, which then
|
|
2607
|
+
// looked like it floated over the middle of the timeline.
|
|
2608
|
+
style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
|
|
1770
2609
|
>
|
|
1771
|
-
{
|
|
1772
|
-
<
|
|
1773
|
-
<
|
|
1774
|
-
{
|
|
1775
|
-
|
|
1776
|
-
|
|
2610
|
+
{onAnnotate ? (
|
|
2611
|
+
<Suspense fallback={null}>
|
|
2612
|
+
<TimelineAnnotationSelection
|
|
2613
|
+
rootRef={scrollRef}
|
|
2614
|
+
sources={annotationSources}
|
|
2615
|
+
onAnnotate={onAnnotate}
|
|
2616
|
+
/>
|
|
2617
|
+
</Suspense>
|
|
1777
2618
|
) : null}
|
|
1778
|
-
{
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2619
|
+
{/* Pinned: anchoring off so the tip-follow camera owns the motion.
|
|
2620
|
+
Unpinned: native scroll anchoring holds the reader's place. */}
|
|
2621
|
+
<div
|
|
2622
|
+
ref={scrollRef}
|
|
2623
|
+
data-og-timeline-scroller=""
|
|
2624
|
+
data-og-bottom-follow={
|
|
2625
|
+
autoFollow && pinned && !hasNewer ? "true" : "false"
|
|
2626
|
+
}
|
|
2627
|
+
tabIndex={-1}
|
|
2628
|
+
onScroll={onScroll}
|
|
2629
|
+
onWheel={onWheel}
|
|
2630
|
+
onTouchStart={(event) => {
|
|
2631
|
+
stopSettlement();
|
|
2632
|
+
const touch = event.touches.length === 1 ? event.touches[0] : undefined;
|
|
2633
|
+
touchPositionRef.current = touch
|
|
2634
|
+
? { x: touch.clientX, y: touch.clientY }
|
|
2635
|
+
: null;
|
|
2636
|
+
}}
|
|
2637
|
+
onTouchMove={(event) => {
|
|
2638
|
+
const touch = event.touches[0];
|
|
2639
|
+
const previous = touchPositionRef.current;
|
|
2640
|
+
if (!touch || !previous || event.touches.length !== 1) {
|
|
2641
|
+
touchPositionRef.current = null;
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
const deltaX = previous.x - touch.clientX;
|
|
2645
|
+
const deltaY = previous.y - touch.clientY;
|
|
2646
|
+
if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
|
|
2647
|
+
touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
|
|
2648
|
+
onWheel({
|
|
2649
|
+
deltaX,
|
|
2650
|
+
deltaY,
|
|
2651
|
+
target: event.target,
|
|
2652
|
+
currentTarget: event.currentTarget,
|
|
2653
|
+
});
|
|
2654
|
+
}}
|
|
2655
|
+
onTouchEnd={() => {
|
|
2656
|
+
touchPositionRef.current = null;
|
|
2657
|
+
}}
|
|
2658
|
+
onTouchCancel={() => {
|
|
2659
|
+
touchPositionRef.current = null;
|
|
2660
|
+
}}
|
|
2661
|
+
onClickCapture={(event) => {
|
|
2662
|
+
const target =
|
|
2663
|
+
event.target instanceof Element
|
|
2664
|
+
? event.target.closest("button[aria-expanded]")
|
|
2665
|
+
: null;
|
|
2666
|
+
if (target) {
|
|
2667
|
+
if (target.hasAttribute("data-og-work-header")) {
|
|
2668
|
+
releaseProgress(
|
|
2669
|
+
target
|
|
2670
|
+
.closest("[data-og-group-key]")
|
|
2671
|
+
?.getAttribute("data-og-group-key") ?? null,
|
|
2672
|
+
);
|
|
1796
2673
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2674
|
+
releasePinFromReader();
|
|
2675
|
+
disclosureKeepsUnpinnedRef.current = true;
|
|
2676
|
+
}
|
|
2677
|
+
}}
|
|
2678
|
+
onFocusCapture={(event) => {
|
|
2679
|
+
if (readableTurns && event.target !== event.currentTarget) {
|
|
2680
|
+
stopSettlement();
|
|
2681
|
+
releasePinFromReader();
|
|
2682
|
+
disclosureKeepsUnpinnedRef.current = true;
|
|
2683
|
+
}
|
|
2684
|
+
}}
|
|
2685
|
+
onPointerDown={onPointerDown}
|
|
2686
|
+
onKeyDown={onKeyDown}
|
|
2687
|
+
style={
|
|
2688
|
+
groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined
|
|
2689
|
+
}
|
|
2690
|
+
className={cn(
|
|
2691
|
+
// tabIndex=-1 is programmatic only — never paint a focus ring on
|
|
2692
|
+
// the whole scroller (click + Shift used to flash a blue outline).
|
|
2693
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
|
|
2694
|
+
autoFollow && pinned && !hasNewer
|
|
2695
|
+
? "[overflow-anchor:none]"
|
|
2696
|
+
: "[overflow-anchor:auto]",
|
|
2697
|
+
)}
|
|
2698
|
+
>
|
|
2699
|
+
<TimelineBeforeLayout
|
|
2700
|
+
capture={() => {
|
|
2701
|
+
if (readableTurns && timelineHasReader(scrollRef.current)) {
|
|
2702
|
+
releasePinFromReader();
|
|
2703
|
+
}
|
|
2704
|
+
readingAnchorRef.current =
|
|
2705
|
+
!pinnedRef.current && scrollRef.current
|
|
2706
|
+
? captureTimelineAnchor(scrollRef.current)
|
|
2707
|
+
: null;
|
|
2708
|
+
settlementRef.current =
|
|
2709
|
+
readableTurns &&
|
|
2710
|
+
autoFollow &&
|
|
2711
|
+
pinnedRef.current &&
|
|
2712
|
+
revealedRef.current &&
|
|
2713
|
+
!hasNewer &&
|
|
2714
|
+
!pendingReaderLeaveRef.current &&
|
|
2715
|
+
!disclosureKeepsUnpinnedRef.current &&
|
|
2716
|
+
!prefersReducedMotion() &&
|
|
2717
|
+
scrollRef.current
|
|
2718
|
+
? captureTimelineSettlement(scrollRef.current, groups)
|
|
2719
|
+
: null;
|
|
1828
2720
|
}}
|
|
1829
|
-
className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
|
|
1830
2721
|
>
|
|
1831
|
-
|
|
1832
|
-
|
|
2722
|
+
<div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
2723
|
+
<AnimatePresence>
|
|
2724
|
+
{loadingOlder ||
|
|
2725
|
+
loadingOldest ||
|
|
2726
|
+
(hasOlder && onJumpToStart && olderPrefetchArmed) ||
|
|
2727
|
+
underfillRetryReady ? (
|
|
2728
|
+
// Reserved top gutter: controls scroll with history and cannot
|
|
2729
|
+
// cover a disclosure. Visibility never changes content height.
|
|
2730
|
+
<motion.div
|
|
2731
|
+
initial={{ opacity: 0, y: -6 }}
|
|
2732
|
+
animate={{ opacity: 1, y: 0 }}
|
|
2733
|
+
exit={{ opacity: 0, y: -6 }}
|
|
2734
|
+
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
2735
|
+
data-og-loading-older=""
|
|
2736
|
+
aria-live="polite"
|
|
2737
|
+
className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
|
|
2738
|
+
>
|
|
2739
|
+
{loadingOlder || loadingOldest ? (
|
|
2740
|
+
<span className={LOADING_CHIP_CLASS}>
|
|
2741
|
+
<span className="og-shimmer-text">
|
|
2742
|
+
{loadingOldest
|
|
2743
|
+
? "Jumping to start…"
|
|
2744
|
+
: "Loading earlier activity…"}
|
|
2745
|
+
</span>
|
|
2746
|
+
</span>
|
|
2747
|
+
) : null}
|
|
2748
|
+
{hasOlder &&
|
|
2749
|
+
!loadingOlder &&
|
|
2750
|
+
!loadingOldest &&
|
|
2751
|
+
(underfillRetryReady || onJumpToStart) ? (
|
|
2752
|
+
<button
|
|
2753
|
+
type="button"
|
|
2754
|
+
data-og-retry={underfillRetryReady || undefined}
|
|
2755
|
+
data-og-jump-to-start={!underfillRetryReady || undefined}
|
|
2756
|
+
onClick={() => {
|
|
2757
|
+
const node = scrollRef.current;
|
|
2758
|
+
if (underfillRetryReady) {
|
|
2759
|
+
if (node && underfillRetryReadyRef.current) {
|
|
2760
|
+
// AnimatePresence retains this handler during
|
|
2761
|
+
// exit. Current authorization plus the exact
|
|
2762
|
+
// attempt check prevent stale dispatch.
|
|
2763
|
+
requestOlderIfUnderfilled(
|
|
2764
|
+
node,
|
|
2765
|
+
underfillSettledAttempt!,
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
applyPinned(false);
|
|
2771
|
+
pendingJumpToStartRef.current = true;
|
|
2772
|
+
const seq = ++jumpToStartSeqRef.current;
|
|
2773
|
+
void Promise.resolve(onJumpToStart!()).then(
|
|
2774
|
+
() => {
|
|
2775
|
+
// The commit that swaps in the oldest window consumes
|
|
2776
|
+
// the flag against the new DOM; this write covers the
|
|
2777
|
+
// already-committed order and the no-window-change
|
|
2778
|
+
// case (jumping within the current window).
|
|
2779
|
+
const scroller = scrollRef.current ?? node;
|
|
2780
|
+
if (scroller) {
|
|
2781
|
+
scroller.scrollTop = 0;
|
|
2782
|
+
}
|
|
2783
|
+
// A host may resolve without ever changing the
|
|
2784
|
+
// window (already on the oldest page). Any swap
|
|
2785
|
+
// commit runs its layout effect before the next
|
|
2786
|
+
// frame, so a flag still armed by then is the
|
|
2787
|
+
// no-change case — clear it, or a LATER prepend
|
|
2788
|
+
// would spuriously jump the reader to the top.
|
|
2789
|
+
requestFrame(() => {
|
|
2790
|
+
if (jumpToStartSeqRef.current === seq) {
|
|
2791
|
+
pendingJumpToStartRef.current = false;
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
},
|
|
2795
|
+
() => {
|
|
2796
|
+
if (jumpToStartSeqRef.current === seq) {
|
|
2797
|
+
pendingJumpToStartRef.current = false;
|
|
2798
|
+
}
|
|
2799
|
+
},
|
|
2800
|
+
);
|
|
2801
|
+
}}
|
|
2802
|
+
className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
|
|
2803
|
+
>
|
|
2804
|
+
{underfillRetryReady
|
|
2805
|
+
? underfillSettledAttempt?.[2]?.tailPreserved
|
|
2806
|
+
? "Load earlier activity"
|
|
2807
|
+
: "Retry earlier activity"
|
|
2808
|
+
: "Jump to start"}
|
|
2809
|
+
</button>
|
|
2810
|
+
) : null}
|
|
2811
|
+
</motion.div>
|
|
2812
|
+
) : null}
|
|
2813
|
+
</AnimatePresence>
|
|
2814
|
+
{!groups.length
|
|
2815
|
+
? (emptyState ?? (
|
|
2816
|
+
<p className="py-10 text-center text-og-menu text-og-fg-subtle">
|
|
2817
|
+
No activity yet.
|
|
2818
|
+
</p>
|
|
2819
|
+
))
|
|
2820
|
+
: null}
|
|
2821
|
+
{hasOlder && olderPrefetchArmed ? (
|
|
2822
|
+
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
2823
|
+
// must never shift content (that shift was itself a wobble).
|
|
2824
|
+
// End at the scroll origin above the pt-16 gutter so the
|
|
2825
|
+
// observer and scrollTop cooldown share the same 400px band.
|
|
2826
|
+
<div
|
|
2827
|
+
ref={topSentinelRef}
|
|
2828
|
+
data-og-top-sentinel=""
|
|
2829
|
+
data-og-timeline-chrome=""
|
|
2830
|
+
aria-hidden="true"
|
|
2831
|
+
className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
|
|
2832
|
+
/>
|
|
2833
|
+
) : null}
|
|
2834
|
+
{groups.map(({ group, key, entranceEnabled }, index) => {
|
|
2835
|
+
return (
|
|
2836
|
+
<TimelineSearchRevealContext.Provider
|
|
2837
|
+
key={key}
|
|
2838
|
+
value={
|
|
2839
|
+
searchItem &&
|
|
2840
|
+
timelineGroupItemIds(group).includes(searchItem.id)
|
|
2841
|
+
? searchRevealKey
|
|
2842
|
+
: null
|
|
2843
|
+
}
|
|
2844
|
+
>
|
|
2845
|
+
<TimelineGroupEntry
|
|
2846
|
+
groupKey={key}
|
|
2847
|
+
group={group}
|
|
2848
|
+
nextGroup={groups[index + 1]?.group}
|
|
2849
|
+
startupDismissed={
|
|
2850
|
+
group.kind === "activity" &&
|
|
2851
|
+
group.items.some(
|
|
2852
|
+
(item) => item.turnId && turnsWithOutput.has(item.turnId),
|
|
2853
|
+
)
|
|
2854
|
+
}
|
|
2855
|
+
entranceEnabled={entranceEnabled}
|
|
2856
|
+
liveEntranceEnabled={
|
|
2857
|
+
group.kind === "activity" ? !bulkRender : undefined
|
|
2858
|
+
}
|
|
2859
|
+
noteFolded={
|
|
2860
|
+
group.kind === "item"
|
|
2861
|
+
? liveNoteFolds.get(group.item.id)
|
|
2862
|
+
: undefined
|
|
2863
|
+
}
|
|
2864
|
+
context={timelineGroupEntryContext}
|
|
2865
|
+
/>
|
|
2866
|
+
</TimelineSearchRevealContext.Provider>
|
|
2867
|
+
);
|
|
2868
|
+
})}
|
|
2869
|
+
{groups.length > 0 && trailingState ? (
|
|
2870
|
+
<div data-og-timeline-trailing-state="">{trailingState}</div>
|
|
2871
|
+
) : null}
|
|
2872
|
+
{hasNewer && newerFailure?.key === newerBoundaryKey ? (
|
|
2873
|
+
<div
|
|
2874
|
+
data-og-newer-error=""
|
|
2875
|
+
className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
|
|
2876
|
+
>
|
|
2877
|
+
<p role="status" className="max-w-prose [overflow-wrap:anywhere]">
|
|
2878
|
+
Couldn’t load later activity. {newerFailure.message}
|
|
2879
|
+
</p>
|
|
2880
|
+
<button
|
|
2881
|
+
ref={newerRetryButtonRef}
|
|
2882
|
+
type="button"
|
|
2883
|
+
data-og-retry-newer=""
|
|
2884
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
|
|
2885
|
+
aria-disabled={loadingNewer || newerRetryPending}
|
|
2886
|
+
aria-busy={newerRetryPending}
|
|
2887
|
+
onClick={() => requestNewer(true)}
|
|
2888
|
+
>
|
|
2889
|
+
{newerRetryPending
|
|
2890
|
+
? "Retrying later activity…"
|
|
2891
|
+
: "Retry later activity"}
|
|
2892
|
+
</button>
|
|
2893
|
+
</div>
|
|
2894
|
+
) : null}
|
|
2895
|
+
{hasNewer ? (
|
|
2896
|
+
<div
|
|
2897
|
+
ref={bottomSentinelRef}
|
|
2898
|
+
data-og-bottom-sentinel=""
|
|
2899
|
+
data-og-timeline-chrome=""
|
|
2900
|
+
aria-hidden="true"
|
|
2901
|
+
className="h-px w-full"
|
|
2902
|
+
/>
|
|
2903
|
+
) : null}
|
|
2904
|
+
</div>
|
|
2905
|
+
</TimelineBeforeLayout>
|
|
2906
|
+
</div>
|
|
2907
|
+
{draftAnnotations && draftAnnotations.length > 0 ? (
|
|
2908
|
+
<Suspense fallback={null}>
|
|
2909
|
+
<TimelineAnnotationMarkers
|
|
2910
|
+
rootRef={scrollRef}
|
|
2911
|
+
annotations={draftAnnotations}
|
|
2912
|
+
onSelect={onDraftAnnotationSelect}
|
|
2913
|
+
/>
|
|
2914
|
+
</Suspense>
|
|
1833
2915
|
) : null}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2916
|
+
|
|
2917
|
+
<div
|
|
2918
|
+
data-og-timeline-nav="top"
|
|
2919
|
+
className={cn(
|
|
2920
|
+
"pointer-events-none absolute inset-x-0 z-10 flex justify-center",
|
|
2921
|
+
questionNav?.belowHeader ? "top-10 pointer-coarse:top-13" : "top-3",
|
|
2922
|
+
)}
|
|
2923
|
+
>
|
|
2924
|
+
<AnimatePresence>
|
|
2925
|
+
{questionNav ? (
|
|
2926
|
+
<motion.button
|
|
2927
|
+
key="question-nav"
|
|
2928
|
+
{...NAV_FADE}
|
|
2929
|
+
type="button"
|
|
2930
|
+
data-og-question-nav=""
|
|
2931
|
+
data-og-jump-to-question=""
|
|
2932
|
+
title="Back to your message"
|
|
2933
|
+
onClick={() => jumpToQuestion(questionNav.key)}
|
|
2934
|
+
className={NAV_BUTTON_CLASS}
|
|
2935
|
+
>
|
|
2936
|
+
<ArrowUpIcon aria-hidden className="size-4" />
|
|
2937
|
+
<span className="sr-only">Back to your message</span>
|
|
2938
|
+
</motion.button>
|
|
2939
|
+
) : null}
|
|
2940
|
+
</AnimatePresence>
|
|
2941
|
+
</div>
|
|
2942
|
+
<div
|
|
2943
|
+
data-og-timeline-nav="bottom"
|
|
2944
|
+
className="pointer-events-none absolute inset-x-0 bottom-3 z-10 flex flex-col items-center gap-2"
|
|
2945
|
+
>
|
|
2946
|
+
<AnimatePresence>
|
|
2947
|
+
{loadingNewer ? (
|
|
2948
|
+
<motion.div
|
|
2949
|
+
key="loading-newer"
|
|
2950
|
+
{...NAV_FADE}
|
|
2951
|
+
data-og-loading-newer=""
|
|
2952
|
+
aria-live="polite"
|
|
2953
|
+
className="flex max-w-full justify-center"
|
|
2954
|
+
>
|
|
2955
|
+
<span className={LOADING_CHIP_CLASS}>
|
|
2956
|
+
<span className="og-shimmer-text">Loading later activity…</span>
|
|
2957
|
+
</span>
|
|
2958
|
+
</motion.div>
|
|
2959
|
+
) : null}
|
|
2960
|
+
{jumpPillShown ? (
|
|
2961
|
+
<motion.button
|
|
2962
|
+
key="jump-to-latest"
|
|
2963
|
+
{...NAV_FADE}
|
|
2964
|
+
type="button"
|
|
2965
|
+
data-og-jump-to-latest=""
|
|
2966
|
+
title="Jump to latest"
|
|
2967
|
+
onClick={() => {
|
|
2968
|
+
// Returning to the tip explicitly releases reader-owned
|
|
2969
|
+
// prose. Clear only this timeline's selection before the
|
|
2970
|
+
// synchronous ownership check on the next commit.
|
|
2971
|
+
const viewport = scrollRef.current;
|
|
2972
|
+
const selection = viewport?.ownerDocument.getSelection();
|
|
2973
|
+
if (
|
|
2974
|
+
selection &&
|
|
2975
|
+
!selection.isCollapsed &&
|
|
2976
|
+
(viewport?.contains(selection.anchorNode) ||
|
|
2977
|
+
viewport?.contains(selection.focusNode))
|
|
2978
|
+
)
|
|
2979
|
+
selection.removeAllRanges();
|
|
2980
|
+
disclosureKeepsUnpinnedRef.current = false;
|
|
2981
|
+
if (hasNewer) {
|
|
2982
|
+
// Do not pin against the current history page — its bottom
|
|
2983
|
+
// is not the tip. The pin + snap run when the tip window
|
|
2984
|
+
// actually lands (`hasNewer` flips false).
|
|
2985
|
+
wantPinRef.current = true;
|
|
2986
|
+
const node = scrollRef.current;
|
|
2987
|
+
if (onJumpToLatest) {
|
|
2988
|
+
void Promise.resolve(onJumpToLatest()).then(
|
|
2989
|
+
() => {
|
|
2990
|
+
// Covers a host that flipped hasNewer before
|
|
2991
|
+
// resolving; otherwise the tip-window commit
|
|
2992
|
+
// consumes the flag.
|
|
2993
|
+
const current = scrollRef.current;
|
|
2994
|
+
if (
|
|
2995
|
+
current &&
|
|
2996
|
+
wantPinRef.current &&
|
|
2997
|
+
!hasNewerRef.current
|
|
2998
|
+
) {
|
|
2999
|
+
wantPinRef.current = false;
|
|
3000
|
+
applyPinned(true);
|
|
3001
|
+
snapToBottom(current);
|
|
3002
|
+
}
|
|
3003
|
+
},
|
|
3004
|
+
() => {
|
|
3005
|
+
// The tip reload failed (ordinary network error):
|
|
3006
|
+
// an armed latch would fire a surprise snap when
|
|
3007
|
+
// the reader later pages to the tip themselves.
|
|
3008
|
+
wantPinRef.current = false;
|
|
3009
|
+
},
|
|
3010
|
+
);
|
|
3011
|
+
} else if (node) {
|
|
3012
|
+
// No tip reload available: jump within the in-memory
|
|
3013
|
+
// window so the newer sentinel can page forward; the
|
|
3014
|
+
// latch pins if the tip window eventually lands.
|
|
3015
|
+
snapToBottom(node);
|
|
3016
|
+
}
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const node = scrollRef.current;
|
|
3020
|
+
if (node) {
|
|
1880
3021
|
applyPinned(true);
|
|
1881
|
-
snapToBottom(
|
|
3022
|
+
snapToBottom(node);
|
|
1882
3023
|
}
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
</AnimatePresence>
|
|
1917
|
-
</div>
|
|
1918
|
-
</TooltipProvider>
|
|
1919
|
-
</EntranceAnimationProvider>
|
|
1920
|
-
</TimelineComputeLabelProvider>
|
|
1921
|
-
</SeenActivityIdsProvider>
|
|
1922
|
-
</FoldMemoryProvider>
|
|
1923
|
-
</LightboxProvider>
|
|
3024
|
+
}}
|
|
3025
|
+
className={NAV_BUTTON_CLASS}
|
|
3026
|
+
>
|
|
3027
|
+
<ArrowDownIcon aria-hidden className="size-4" />
|
|
3028
|
+
<span className="sr-only">Jump to latest</span>
|
|
3029
|
+
</motion.button>
|
|
3030
|
+
) : null}
|
|
3031
|
+
</AnimatePresence>
|
|
3032
|
+
</div>
|
|
3033
|
+
</div>
|
|
3034
|
+
</TimelineAnnotationSourceRootContext.Provider>
|
|
3035
|
+
</TooltipProvider>
|
|
3036
|
+
</EntranceAnimationProvider>
|
|
3037
|
+
</TimelineComputeLabelProvider>
|
|
3038
|
+
</SeenActivityIdsProvider>
|
|
3039
|
+
</LiveNoteFoldContext.Provider>
|
|
3040
|
+
</FoldMemoryProvider>
|
|
3041
|
+
</LightboxProvider>
|
|
3042
|
+
</AllowancePresentationProvider>
|
|
3043
|
+
);
|
|
3044
|
+
// Agent-authored object links resolve through the host, never the console.
|
|
3045
|
+
return (
|
|
3046
|
+
<OpenGeniLinkProvider resolveLink={resolveLink}>
|
|
3047
|
+
<MessageSenderContext.Provider value={renderMessageSender}>
|
|
3048
|
+
<AgentIdentityProvider
|
|
3049
|
+
items={resolvedItems}
|
|
3050
|
+
resolveSessionTitle={resolveSessionTitle}
|
|
3051
|
+
onOpenSession={onOpenSession}
|
|
3052
|
+
>
|
|
3053
|
+
<ProviderLogoProvider value={resolveProviderLogo}>{timeline}</ProviderLogoProvider>
|
|
3054
|
+
</AgentIdentityProvider>
|
|
3055
|
+
</MessageSenderContext.Provider>
|
|
3056
|
+
</OpenGeniLinkProvider>
|
|
1924
3057
|
);
|
|
1925
3058
|
}
|
|
1926
3059
|
|
|
1927
3060
|
type KeyedTimelineGroup = {
|
|
1928
3061
|
group: TimelineGroup;
|
|
1929
3062
|
key: string;
|
|
3063
|
+
entranceEnabled: boolean;
|
|
1930
3064
|
};
|
|
1931
3065
|
|
|
3066
|
+
function timelineGroupsRenderEqual(previous: TimelineGroup, next: TimelineGroup): boolean {
|
|
3067
|
+
try {
|
|
3068
|
+
return dequal(previous, next);
|
|
3069
|
+
} catch {
|
|
3070
|
+
// Consumer-owned `unknown` payloads may be proxies/getters. Equality is an
|
|
3071
|
+
// optimization only; a hostile comparator surface must fall back to the
|
|
3072
|
+
// authoritative new group rather than taking down the timeline render.
|
|
3073
|
+
return false;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
1932
3077
|
/**
|
|
1933
3078
|
* Projection can legitimately change a group's content-derived key while
|
|
1934
3079
|
* retaining its existing rows. The common pagination case is an older activity
|
|
@@ -1937,42 +3082,73 @@ type KeyedTimelineGroup = {
|
|
|
1937
3082
|
* committed groups by their durable item IDs so both the React key and the
|
|
1938
3083
|
* progressive-window anchor survive either change.
|
|
1939
3084
|
*/
|
|
1940
|
-
function useStableTimelineGroupKeys(
|
|
3085
|
+
function useStableTimelineGroupKeys(
|
|
3086
|
+
allGroups: TimelineGroup[],
|
|
3087
|
+
entranceEnabled: boolean,
|
|
3088
|
+
): KeyedTimelineGroup[] {
|
|
1941
3089
|
const previousRef = useRef<KeyedTimelineGroup[]>([]);
|
|
1942
3090
|
const keyedGroups = useMemo(() => {
|
|
1943
3091
|
const previousByItemId = new Map<string, KeyedTimelineGroup>();
|
|
1944
3092
|
for (const previous of previousRef.current) {
|
|
1945
3093
|
for (const itemId of timelineGroupItemIds(previous.group)) {
|
|
1946
|
-
|
|
3094
|
+
// A live progress note is both its own row and listed in its work
|
|
3095
|
+
// history. Its own row owns the id, so the note keeps its identity.
|
|
3096
|
+
if (previous.group.kind === "item" || !previousByItemId.has(itemId)) {
|
|
3097
|
+
previousByItemId.set(itemId, previous);
|
|
3098
|
+
}
|
|
1947
3099
|
}
|
|
1948
3100
|
}
|
|
1949
3101
|
|
|
1950
3102
|
const usedKeys = new Set<string>();
|
|
1951
3103
|
return allGroups.map((group, index) => {
|
|
1952
3104
|
const itemIds = timelineGroupItemIds(group);
|
|
1953
|
-
let
|
|
3105
|
+
let retainedGroup: KeyedTimelineGroup | undefined;
|
|
1954
3106
|
for (const itemId of itemIds) {
|
|
1955
3107
|
const previous = previousByItemId.get(itemId);
|
|
1956
3108
|
// Retain only same-kind matches. Activity → turn wrap must NOT keep the
|
|
1957
3109
|
// activity chip's React key: that reused a collapsed TurnSummary and
|
|
1958
3110
|
// skipped the settle beat (insta-collapse / content flash).
|
|
1959
|
-
|
|
1960
|
-
|
|
3111
|
+
const startupCompletion =
|
|
3112
|
+
previous?.group.kind === "activity" &&
|
|
3113
|
+
previous.group.items.every(
|
|
3114
|
+
(item) =>
|
|
3115
|
+
item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()),
|
|
3116
|
+
) &&
|
|
3117
|
+
group.kind === "turn" &&
|
|
3118
|
+
group.outcome === "complete" &&
|
|
3119
|
+
group.groups.every(
|
|
3120
|
+
(child) =>
|
|
3121
|
+
child.kind === "activity" &&
|
|
3122
|
+
child.items.every((item) => item.kind === "startup-phase"),
|
|
3123
|
+
);
|
|
3124
|
+
if (
|
|
3125
|
+
previous &&
|
|
3126
|
+
(previous.group.kind === group.kind || startupCompletion) &&
|
|
3127
|
+
!usedKeys.has(previous.key)
|
|
3128
|
+
) {
|
|
3129
|
+
retainedGroup = previous;
|
|
1961
3130
|
break;
|
|
1962
3131
|
}
|
|
1963
3132
|
}
|
|
1964
3133
|
|
|
1965
3134
|
const canonicalKey = timelineGroupKey(group);
|
|
1966
|
-
let key =
|
|
3135
|
+
let key = retainedGroup?.key ?? canonicalKey;
|
|
1967
3136
|
let collision = 0;
|
|
1968
3137
|
while (usedKeys.has(key)) {
|
|
1969
3138
|
key = `${canonicalKey}:${index}:${collision}`;
|
|
1970
3139
|
collision += 1;
|
|
1971
3140
|
}
|
|
1972
3141
|
usedKeys.add(key);
|
|
1973
|
-
return {
|
|
3142
|
+
return {
|
|
3143
|
+
group:
|
|
3144
|
+
retainedGroup && timelineGroupsRenderEqual(retainedGroup.group, group)
|
|
3145
|
+
? retainedGroup.group
|
|
3146
|
+
: group,
|
|
3147
|
+
key,
|
|
3148
|
+
entranceEnabled: retainedGroup?.entranceEnabled ?? entranceEnabled,
|
|
3149
|
+
};
|
|
1974
3150
|
});
|
|
1975
|
-
}, [allGroups]);
|
|
3151
|
+
}, [allGroups, entranceEnabled]);
|
|
1976
3152
|
|
|
1977
3153
|
useLayoutEffect(() => {
|
|
1978
3154
|
previousRef.current = keyedGroups;
|
|
@@ -2065,17 +3241,155 @@ class TimelineGroupRenderBoundary extends Component<
|
|
|
2065
3241
|
}
|
|
2066
3242
|
}
|
|
2067
3243
|
|
|
3244
|
+
type TimelineGroupEntryProps = {
|
|
3245
|
+
groupKey: string;
|
|
3246
|
+
group: TimelineGroup;
|
|
3247
|
+
nextGroup?: TimelineGroup | undefined;
|
|
3248
|
+
startupDismissed: boolean;
|
|
3249
|
+
entranceEnabled: boolean;
|
|
3250
|
+
liveEntranceEnabled?: boolean | undefined;
|
|
3251
|
+
/**
|
|
3252
|
+
* Present for a live progress note: true while its turn's work disclosure is
|
|
3253
|
+
* open (the note then reads inside it), false while it reads here.
|
|
3254
|
+
*/
|
|
3255
|
+
noteFolded?: boolean | undefined;
|
|
3256
|
+
context: TimelineGroupEntryContext;
|
|
3257
|
+
};
|
|
3258
|
+
|
|
3259
|
+
type TimelineGroupEntryContext = {
|
|
3260
|
+
userMessageDisclosureContext: UserMessageDisclosureContextValue;
|
|
3261
|
+
behavior: TimelineGroupBehaviorProps;
|
|
3262
|
+
};
|
|
3263
|
+
|
|
3264
|
+
type TimelineGroupBehaviorProps = {
|
|
3265
|
+
renderMessageActions: MessageTimelineProps["renderMessageActions"];
|
|
3266
|
+
renderMessageText:
|
|
3267
|
+
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
3268
|
+
| undefined;
|
|
3269
|
+
onOpenSession: MessageTimelineProps["onOpenSession"];
|
|
3270
|
+
onMemoryClick: MessageTimelineProps["onMemoryClick"];
|
|
3271
|
+
onReconnect: MessageTimelineProps["onReconnect"];
|
|
3272
|
+
renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"];
|
|
3273
|
+
resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
|
|
3274
|
+
toolRegistry: ToolRegistry;
|
|
3275
|
+
loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
|
|
3276
|
+
loadRetainedArtifact: MessageTimelineProps["loadRetainedArtifact"];
|
|
3277
|
+
loadVideoArtifactPlayback: MessageTimelineProps["loadVideoArtifactPlayback"];
|
|
3278
|
+
genieLoading: MessageTimelineProps["genieLoading"];
|
|
3279
|
+
turnSummary: MessageTimelineProps["turnSummary"];
|
|
3280
|
+
};
|
|
3281
|
+
|
|
3282
|
+
/**
|
|
3283
|
+
* Keep the complete settled-row shell behind one shallow memo boundary. A
|
|
3284
|
+
* prepend still reconciles the keyed list, but render-equivalent suffix groups
|
|
3285
|
+
* skip their providers, error boundaries, disclosure wrappers, and row trees.
|
|
3286
|
+
*/
|
|
3287
|
+
const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
3288
|
+
groupKey,
|
|
3289
|
+
group,
|
|
3290
|
+
nextGroup,
|
|
3291
|
+
startupDismissed,
|
|
3292
|
+
entranceEnabled,
|
|
3293
|
+
liveEntranceEnabled,
|
|
3294
|
+
noteFolded,
|
|
3295
|
+
context,
|
|
3296
|
+
}: TimelineGroupEntryProps) {
|
|
3297
|
+
const reducedMotion = useReducedMotion();
|
|
3298
|
+
const { behavior, userMessageDisclosureContext } = context;
|
|
3299
|
+
const contextCompactionCount =
|
|
3300
|
+
group.kind === "turn"
|
|
3301
|
+
? (group.contextCompactionCount ?? 0)
|
|
3302
|
+
: group.kind === "activity" &&
|
|
3303
|
+
nextGroup?.kind === "item" &&
|
|
3304
|
+
nextGroup.item.kind === "context-compaction" &&
|
|
3305
|
+
nextGroup.item.phase === "compacted"
|
|
3306
|
+
? 1
|
|
3307
|
+
: 0;
|
|
3308
|
+
const content = (
|
|
3309
|
+
<ActivityNoteTextContext.Provider value={behavior.renderMessageText ?? null}>
|
|
3310
|
+
<TimelineGroupView
|
|
3311
|
+
{...behavior}
|
|
3312
|
+
group={group}
|
|
3313
|
+
foldLiveCluster={isAgentProgress(nextGroup)}
|
|
3314
|
+
startupDismissed={startupDismissed}
|
|
3315
|
+
trailingAgentText={trailingAgentTextAfterTurn(group, nextGroup)}
|
|
3316
|
+
contextCompactionCount={contextCompactionCount > 0 ? contextCompactionCount : undefined}
|
|
3317
|
+
/>
|
|
3318
|
+
</ActivityNoteTextContext.Provider>
|
|
3319
|
+
);
|
|
3320
|
+
return (
|
|
3321
|
+
<GenieLoadingOptionsContext.Provider value={behavior.genieLoading}>
|
|
3322
|
+
<div
|
|
3323
|
+
data-og-timeline-group-anchor=""
|
|
3324
|
+
data-og-group-key={groupKey}
|
|
3325
|
+
data-og-prompt={
|
|
3326
|
+
group.kind === "item" && group.item.kind === "user-message" ? "" : undefined
|
|
3327
|
+
}
|
|
3328
|
+
tabIndex={group.kind === "item" && group.item.kind === "user-message" ? -1 : undefined}
|
|
3329
|
+
// A live progress note reads inside its open work row instead; folded,
|
|
3330
|
+
// it takes no space and also gives back the list's gap-5 above it.
|
|
3331
|
+
data-og-live-note={noteFolded === undefined ? undefined : noteFolded ? "folded" : "shown"}
|
|
3332
|
+
data-og-live-note-id={
|
|
3333
|
+
noteFolded !== undefined && group.kind === "item" ? group.item.id : undefined
|
|
3334
|
+
}
|
|
3335
|
+
aria-hidden={noteFolded || undefined}
|
|
3336
|
+
inert={noteFolded || undefined}
|
|
3337
|
+
className={
|
|
3338
|
+
noteFolded ? "-mt-5 h-0 overflow-hidden opacity-0 [overflow-anchor:none]" : undefined
|
|
3339
|
+
}
|
|
3340
|
+
>
|
|
3341
|
+
<EntranceAnimationProvider value={entranceEnabled} liveValue={liveEntranceEnabled}>
|
|
3342
|
+
<TimelineGroupRenderBoundary resetKeys={[group, behavior]}>
|
|
3343
|
+
<UserMessageDisclosureProvider value={userMessageDisclosureContext}>
|
|
3344
|
+
{/* Item groups never switch the preparation/content key. Keep their
|
|
3345
|
+
DOM shell without mounting inert presence and motion lifecycles
|
|
3346
|
+
for every historical message in a prepend. */}
|
|
3347
|
+
{group.kind === "item" ? (
|
|
3348
|
+
<div>{content}</div>
|
|
3349
|
+
) : (
|
|
3350
|
+
<AnimatePresence initial={false}>
|
|
3351
|
+
<motion.div
|
|
3352
|
+
key={
|
|
3353
|
+
!startupDismissed &&
|
|
3354
|
+
group.kind === "activity" &&
|
|
3355
|
+
!group.work &&
|
|
3356
|
+
group.items.every(
|
|
3357
|
+
(item) =>
|
|
3358
|
+
item.kind === "startup-phase" ||
|
|
3359
|
+
(item.kind === "reasoning" && !item.text.trim()),
|
|
3360
|
+
)
|
|
3361
|
+
? "preparation"
|
|
3362
|
+
: "content"
|
|
3363
|
+
}
|
|
3364
|
+
initial={false}
|
|
3365
|
+
exit={{ opacity: 0, height: 0 }}
|
|
3366
|
+
transition={{ duration: reducedMotion ? 0 : 0.2 }}
|
|
3367
|
+
>
|
|
3368
|
+
{content}
|
|
3369
|
+
</motion.div>
|
|
3370
|
+
</AnimatePresence>
|
|
3371
|
+
)}
|
|
3372
|
+
</UserMessageDisclosureProvider>
|
|
3373
|
+
</TimelineGroupRenderBoundary>
|
|
3374
|
+
</EntranceAnimationProvider>
|
|
3375
|
+
</div>
|
|
3376
|
+
</GenieLoadingOptionsContext.Provider>
|
|
3377
|
+
);
|
|
3378
|
+
});
|
|
3379
|
+
|
|
2068
3380
|
// The full loaded window stays mounted, so settled history rows must be cheap
|
|
2069
|
-
// on every commit: projection reuses group
|
|
2070
|
-
// memo skips them.
|
|
2071
|
-
// behavior/callback changes are separate props, so ordinary
|
|
2072
|
-
// updates still invalidate immediately.
|
|
3381
|
+
// on every commit: the stable-key projection reuses render-equivalent group
|
|
3382
|
+
// objects, so memo skips them. Changed projection content receives a new group
|
|
3383
|
+
// object, and behavior/callback changes are separate props, so ordinary
|
|
3384
|
+
// streaming and host updates still invalidate immediately.
|
|
2073
3385
|
const TimelineGroupView = memo(function TimelineGroupView({
|
|
2074
3386
|
group,
|
|
3387
|
+
renderMessageActions,
|
|
2075
3388
|
renderMessageText,
|
|
2076
3389
|
onOpenSession,
|
|
2077
3390
|
onMemoryClick,
|
|
2078
3391
|
onReconnect,
|
|
3392
|
+
renderAuthNeeded,
|
|
2079
3393
|
resolveProviderLogo,
|
|
2080
3394
|
toolRegistry,
|
|
2081
3395
|
loadRetainedScreenshot,
|
|
@@ -2085,16 +3399,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2085
3399
|
insideTurn = false,
|
|
2086
3400
|
nestClusterChips = false,
|
|
2087
3401
|
foldLiveCluster = false,
|
|
3402
|
+
startupDismissed = false,
|
|
2088
3403
|
trailingAgentText,
|
|
2089
3404
|
contextCompactionCount,
|
|
2090
3405
|
}: {
|
|
2091
3406
|
group: TimelineGroup;
|
|
3407
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2092
3408
|
renderMessageText?:
|
|
2093
3409
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2094
3410
|
| undefined;
|
|
2095
3411
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2096
3412
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
2097
3413
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
3414
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
3415
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
2098
3416
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2099
3417
|
toolRegistry: ToolRegistry;
|
|
2100
3418
|
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
@@ -2105,6 +3423,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2105
3423
|
behind a neutral chip — the one place activity without an outcome still
|
|
2106
3424
|
folds, bounding the DOM of days-long autonomous turns. */
|
|
2107
3425
|
foldLiveCluster?: boolean;
|
|
3426
|
+
startupDismissed?: boolean;
|
|
2108
3427
|
/** Rendering inside an expanded turn group: the outer chip already owns the
|
|
2109
3428
|
failure surface, so nested chips stay tinted but quiet (no repeated
|
|
2110
3429
|
failure text, no auto-open) — one loud error, N calm sub-expands. */
|
|
@@ -2125,15 +3444,29 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2125
3444
|
/** Secondary chip facet when this fold sits next to a compaction landmark. */
|
|
2126
3445
|
contextCompactionCount?: number | undefined;
|
|
2127
3446
|
}) {
|
|
3447
|
+
const startupDetails = useStartupDetails();
|
|
2128
3448
|
const enter = useEntranceAnimation();
|
|
2129
3449
|
const settleChrome = useTurnSettleOpen();
|
|
3450
|
+
const previousSingleActivity = useRef<ActivityItem | undefined>(undefined);
|
|
3451
|
+
useLayoutEffect(() => {
|
|
3452
|
+
const work =
|
|
3453
|
+
group.kind === "activity" ? group.items.filter((item) => item.kind !== "startup-phase") : [];
|
|
3454
|
+
previousSingleActivity.current = work.length === 1 ? work[0] : undefined;
|
|
3455
|
+
}, [group]);
|
|
2130
3456
|
const foldMemory = useFoldMemory();
|
|
3457
|
+
const reportLiveFold = useContext(LiveNoteFoldContext);
|
|
2131
3458
|
// Settled (or live-fold) activity clusters get a chip. Inside an expanded
|
|
2132
3459
|
// turn that is the second layer — quiet nested chips under the outer turn
|
|
2133
3460
|
// summary when contiguous activity naturally clusters (≥2 only).
|
|
3461
|
+
const containsPresentedImage = timelineGroupContainsPresentedImage(group);
|
|
3462
|
+
const hasRememberedImageFold =
|
|
3463
|
+
group.kind === "activity" && containsPresentedImage && foldMemory?.has(group.id);
|
|
3464
|
+
// Primary images stay visible through live narration and the turn wrap.
|
|
3465
|
+
// Manual collapse still belongs to TurnSummary's existing fold memory.
|
|
2134
3466
|
const activityShouldFold =
|
|
2135
|
-
group.kind === "activity" &&
|
|
2136
|
-
|
|
3467
|
+
group.kind === "activity" &&
|
|
3468
|
+
!containsPresentedImage &&
|
|
3469
|
+
!!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
|
|
2137
3470
|
// Latch live→folded so a top-level shell that was already mounted open can
|
|
2138
3471
|
// start the settle beat without remounting bare rail → wrapper.
|
|
2139
3472
|
const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn);
|
|
@@ -2142,7 +3475,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2142
3475
|
group.kind === "turn" &&
|
|
2143
3476
|
(group.outcome === "failed" ||
|
|
2144
3477
|
timelineGroupContainsAuthNeeded(group) ||
|
|
2145
|
-
|
|
3478
|
+
containsPresentedImage);
|
|
2146
3479
|
// activity-* → turn-* remount: carry resting state so settleFold does not
|
|
2147
3480
|
// re-open a chip the reader already watched collapse.
|
|
2148
3481
|
if (group.kind === "turn" && foldMemory && !insideTurn) {
|
|
@@ -2153,9 +3486,167 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2153
3486
|
);
|
|
2154
3487
|
}
|
|
2155
3488
|
const settleFold =
|
|
2156
|
-
|
|
3489
|
+
!turnSummary?.rolling &&
|
|
3490
|
+
(group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
|
|
2157
3491
|
switch (group.kind) {
|
|
2158
3492
|
case "activity":
|
|
3493
|
+
if (group.work) {
|
|
3494
|
+
const preparing = isPreparingWork(group, { startupDetails, startupDismissed });
|
|
3495
|
+
if (preparing) {
|
|
3496
|
+
// Preparation is primary, never hidden in an activity disclosure.
|
|
3497
|
+
// Keep work.startedAt unchanged for the handoff and settled duration.
|
|
3498
|
+
return (
|
|
3499
|
+
<ActivityRail
|
|
3500
|
+
items={group.items}
|
|
3501
|
+
startupActive
|
|
3502
|
+
bare
|
|
3503
|
+
toolRegistry={toolRegistry}
|
|
3504
|
+
onOpenSession={onOpenSession}
|
|
3505
|
+
onMemoryClick={onMemoryClick}
|
|
3506
|
+
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
3507
|
+
loadRetainedArtifact={loadRetainedArtifact}
|
|
3508
|
+
/>
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
const workStatus = readableWorkStatus({ ...group, work: group.work });
|
|
3512
|
+
const status: TurnSummaryStatus =
|
|
3513
|
+
workStatus.kind === "working"
|
|
3514
|
+
? {
|
|
3515
|
+
...workStatus,
|
|
3516
|
+
preview: readableWorkShowsPreview(group) ? (
|
|
3517
|
+
<RollingActivity
|
|
3518
|
+
items={group.items}
|
|
3519
|
+
toolRegistry={toolRegistry}
|
|
3520
|
+
showCount={false}
|
|
3521
|
+
/>
|
|
3522
|
+
) : undefined,
|
|
3523
|
+
}
|
|
3524
|
+
: workStatus.kind === "waiting"
|
|
3525
|
+
? {
|
|
3526
|
+
kind: "waiting",
|
|
3527
|
+
label: workStatus.label,
|
|
3528
|
+
since: workStatus.since,
|
|
3529
|
+
// One quiet line under the row saying what the wait needs,
|
|
3530
|
+
// kept visible when the work is expanded; never a second
|
|
3531
|
+
// warning banner.
|
|
3532
|
+
guidance: workStatus.detail ? (
|
|
3533
|
+
<span
|
|
3534
|
+
data-og-exchange-wait-detail=""
|
|
3535
|
+
className="block text-og-sm text-og-fg-subtle"
|
|
3536
|
+
>
|
|
3537
|
+
{workStatus.detail}
|
|
3538
|
+
</span>
|
|
3539
|
+
) : undefined,
|
|
3540
|
+
}
|
|
3541
|
+
: group.work.cycles?.summary
|
|
3542
|
+
? {
|
|
3543
|
+
...workStatus,
|
|
3544
|
+
// Folded quiet cycles say what the agent is waiting on.
|
|
3545
|
+
preview: (
|
|
3546
|
+
<p
|
|
3547
|
+
data-og-cycles-summary=""
|
|
3548
|
+
className="truncate text-og-sm text-og-fg-subtle"
|
|
3549
|
+
title={group.work.cycles.summary}
|
|
3550
|
+
>
|
|
3551
|
+
{group.work.cycles.summary}
|
|
3552
|
+
</p>
|
|
3553
|
+
),
|
|
3554
|
+
}
|
|
3555
|
+
: workStatus;
|
|
3556
|
+
return (
|
|
3557
|
+
// Marks this work row for the live-note fold (list keys can be carried
|
|
3558
|
+
// over from an earlier row, so they are not the work id).
|
|
3559
|
+
<div className="contents" data-og-work-id={group.id}>
|
|
3560
|
+
<TurnSummary
|
|
3561
|
+
key="work"
|
|
3562
|
+
items={group.items}
|
|
3563
|
+
bare={insideTurn || undefined}
|
|
3564
|
+
status={status}
|
|
3565
|
+
// A wait before any visible work has nothing to reveal.
|
|
3566
|
+
expandable={readableWorkHasDetails(
|
|
3567
|
+
{ ...group, work: group.work },
|
|
3568
|
+
{ startupDetails },
|
|
3569
|
+
)}
|
|
3570
|
+
outcome={group.outcome}
|
|
3571
|
+
failureText={group.failureText}
|
|
3572
|
+
foldKey={group.id}
|
|
3573
|
+
defaultOpen={readableWorkDefaultOpen(group)}
|
|
3574
|
+
facets={turnSummary?.facets}
|
|
3575
|
+
contextCompactionCount={compactedLandmarkCount(group.work.details)}
|
|
3576
|
+
onOpenStateChange={
|
|
3577
|
+
group.work.liveNoteIds?.length && reportLiveFold
|
|
3578
|
+
? (open) => reportLiveFold(group.id, open)
|
|
3579
|
+
: undefined
|
|
3580
|
+
}
|
|
3581
|
+
>
|
|
3582
|
+
<TurnRailFrame compact>
|
|
3583
|
+
{renderFoldedGroups(
|
|
3584
|
+
group.work.details,
|
|
3585
|
+
{
|
|
3586
|
+
renderMessageActions,
|
|
3587
|
+
renderMessageText,
|
|
3588
|
+
onOpenSession,
|
|
3589
|
+
onMemoryClick,
|
|
3590
|
+
onReconnect,
|
|
3591
|
+
renderAuthNeeded,
|
|
3592
|
+
resolveProviderLogo,
|
|
3593
|
+
toolRegistry,
|
|
3594
|
+
loadRetainedScreenshot,
|
|
3595
|
+
loadRetainedArtifact,
|
|
3596
|
+
loadVideoArtifactPlayback,
|
|
3597
|
+
turnSummary,
|
|
3598
|
+
},
|
|
3599
|
+
true,
|
|
3600
|
+
)}
|
|
3601
|
+
</TurnRailFrame>
|
|
3602
|
+
</TurnSummary>
|
|
3603
|
+
</div>
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
// Preparation is one quiet surface, not a fold with eight technical steps.
|
|
3607
|
+
// A later turn of the same exchange keeps its status row instead.
|
|
3608
|
+
if (
|
|
3609
|
+
!startupDetails &&
|
|
3610
|
+
!insideTurn &&
|
|
3611
|
+
!group.outcome &&
|
|
3612
|
+
group.items.every(
|
|
3613
|
+
(item) =>
|
|
3614
|
+
item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()),
|
|
3615
|
+
)
|
|
3616
|
+
) {
|
|
3617
|
+
return (
|
|
3618
|
+
<ActivityRail
|
|
3619
|
+
items={group.items}
|
|
3620
|
+
startupActive={!startupDismissed && !foldLiveCluster}
|
|
3621
|
+
bare
|
|
3622
|
+
toolRegistry={toolRegistry}
|
|
3623
|
+
onOpenSession={onOpenSession}
|
|
3624
|
+
onMemoryClick={onMemoryClick}
|
|
3625
|
+
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
3626
|
+
loadRetainedArtifact={loadRetainedArtifact}
|
|
3627
|
+
/>
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
if (
|
|
3631
|
+
turnSummary?.rolling &&
|
|
3632
|
+
insideTurn &&
|
|
3633
|
+
!startupDetails &&
|
|
3634
|
+
!hasRememberedImageFold &&
|
|
3635
|
+
group.items.filter((item) => item.kind !== "startup-phase").length === 1
|
|
3636
|
+
) {
|
|
3637
|
+
return (
|
|
3638
|
+
<ActivityRail
|
|
3639
|
+
items={group.items}
|
|
3640
|
+
startupActive={false}
|
|
3641
|
+
bare
|
|
3642
|
+
toolRegistry={toolRegistry}
|
|
3643
|
+
onOpenSession={onOpenSession}
|
|
3644
|
+
onMemoryClick={onMemoryClick}
|
|
3645
|
+
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
3646
|
+
loadRetainedArtifact={loadRetainedArtifact}
|
|
3647
|
+
/>
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
2159
3650
|
if (insideTurn) {
|
|
2160
3651
|
// Nested chips whenever the parent has ≥2 clusters. During outer settle
|
|
2161
3652
|
// chrome they stay force-open so structure is visible and height stays
|
|
@@ -2167,11 +3658,22 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2167
3658
|
// settled closed pre-wrap was showing as a CHIP, so mounting it closed
|
|
2168
3659
|
// is both the stable height and the honest state — force-opening it
|
|
2169
3660
|
// was the "already-collapsed cluster auto-expands at the end" reopen.
|
|
2170
|
-
const
|
|
3661
|
+
const visibleActivity = group.items.filter((item) => item.kind !== "startup-phase");
|
|
3662
|
+
const singleThought =
|
|
3663
|
+
!startupDetails &&
|
|
3664
|
+
visibleActivity.length === 1 &&
|
|
3665
|
+
visibleActivity[0]?.kind === "reasoning";
|
|
3666
|
+
// Untouched images stay primary output, but a reader-owned image fold
|
|
3667
|
+
// must keep its shell across a multi-cluster wrap. A bare rail would
|
|
3668
|
+
// bypass the remembered choice; retaining either state also lets the
|
|
3669
|
+
// reader reopen and close the same chip after settlement.
|
|
3670
|
+
const useNestedChip =
|
|
3671
|
+
nestClusterChips && (activityShouldFold || hasRememberedImageFold) && !singleThought;
|
|
2171
3672
|
if (!useNestedChip) {
|
|
2172
3673
|
return (
|
|
2173
3674
|
<ActivityRail
|
|
2174
3675
|
items={group.items}
|
|
3676
|
+
{...(startupDismissed ? { startupActive: false } : {})}
|
|
2175
3677
|
onOpenSession={onOpenSession}
|
|
2176
3678
|
onMemoryClick={onMemoryClick}
|
|
2177
3679
|
toolRegistry={toolRegistry}
|
|
@@ -2209,10 +3711,31 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2209
3711
|
// flips settleFold (collapse) instead of remounting bare rail → wrapper.
|
|
2210
3712
|
return (
|
|
2211
3713
|
<TurnSummary
|
|
3714
|
+
// Apply the primary-output default when a rolling activity first
|
|
3715
|
+
// produces an image; the stable foldKey still preserves user choices.
|
|
3716
|
+
key={containsPresentedImage ? "primary-image" : "activity"}
|
|
2212
3717
|
items={group.items}
|
|
2213
3718
|
outcome={group.outcome}
|
|
2214
3719
|
failureText={group.failureText}
|
|
2215
|
-
defaultOpen={
|
|
3720
|
+
defaultOpen={
|
|
3721
|
+
group.outcome === "failed" ||
|
|
3722
|
+
containsPresentedImage ||
|
|
3723
|
+
(!turnSummary?.rolling && !activityShouldFold)
|
|
3724
|
+
? true
|
|
3725
|
+
: undefined
|
|
3726
|
+
}
|
|
3727
|
+
liveHeader={
|
|
3728
|
+
turnSummary?.rolling &&
|
|
3729
|
+
!containsPresentedImage &&
|
|
3730
|
+
!group.outcome &&
|
|
3731
|
+
!foldLiveCluster ? (
|
|
3732
|
+
<RollingActivity
|
|
3733
|
+
items={group.items}
|
|
3734
|
+
toolRegistry={toolRegistry}
|
|
3735
|
+
previousItem={previousSingleActivity.current}
|
|
3736
|
+
/>
|
|
3737
|
+
) : undefined
|
|
3738
|
+
}
|
|
2216
3739
|
foldKey={group.id}
|
|
2217
3740
|
facets={turnSummary?.facets}
|
|
2218
3741
|
settleFold={settleFold}
|
|
@@ -2235,54 +3758,40 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2235
3758
|
);
|
|
2236
3759
|
case "turn": {
|
|
2237
3760
|
const activityItems = flattenActivityItems(group.groups);
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
3761
|
+
if (
|
|
3762
|
+
!startupDetails &&
|
|
3763
|
+
group.outcome === "complete" &&
|
|
3764
|
+
group.groups.every(
|
|
3765
|
+
(child) =>
|
|
3766
|
+
child.kind === "activity" &&
|
|
3767
|
+
child.items.every(
|
|
3768
|
+
(item) => item.kind === "startup-phase" && item.status === "complete",
|
|
3769
|
+
),
|
|
3770
|
+
)
|
|
3771
|
+
)
|
|
3772
|
+
return <ActivityRail items={activityItems} startupActive={false} bare />;
|
|
2242
3773
|
const turnCopyText = collectTurnCopyText(group.groups, trailingAgentText);
|
|
2243
|
-
const body = group.groups
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
loadRetainedScreenshot,
|
|
2257
|
-
loadRetainedArtifact,
|
|
2258
|
-
loadVideoArtifactPlayback,
|
|
2259
|
-
turnSummary,
|
|
2260
|
-
]}
|
|
2261
|
-
>
|
|
2262
|
-
<TimelineGroupView
|
|
2263
|
-
group={child}
|
|
2264
|
-
renderMessageText={renderMessageText}
|
|
2265
|
-
onOpenSession={onOpenSession}
|
|
2266
|
-
onMemoryClick={onMemoryClick}
|
|
2267
|
-
onReconnect={onReconnect}
|
|
2268
|
-
resolveProviderLogo={resolveProviderLogo}
|
|
2269
|
-
toolRegistry={toolRegistry}
|
|
2270
|
-
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
2271
|
-
loadRetainedArtifact={loadRetainedArtifact}
|
|
2272
|
-
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
2273
|
-
turnSummary={turnSummary}
|
|
2274
|
-
insideTurn
|
|
2275
|
-
nestClusterChips={nestClusters}
|
|
2276
|
-
/>
|
|
2277
|
-
</TimelineGroupRenderBoundary>
|
|
2278
|
-
);
|
|
3774
|
+
const body = renderFoldedGroups(group.groups, {
|
|
3775
|
+
renderMessageActions,
|
|
3776
|
+
renderMessageText,
|
|
3777
|
+
onOpenSession,
|
|
3778
|
+
onMemoryClick,
|
|
3779
|
+
onReconnect,
|
|
3780
|
+
renderAuthNeeded,
|
|
3781
|
+
resolveProviderLogo,
|
|
3782
|
+
toolRegistry,
|
|
3783
|
+
loadRetainedScreenshot,
|
|
3784
|
+
loadRetainedArtifact,
|
|
3785
|
+
loadVideoArtifactPlayback,
|
|
3786
|
+
turnSummary,
|
|
2279
3787
|
});
|
|
3788
|
+
const durationMs = durationBetween(group.startedAt, group.endedAt);
|
|
2280
3789
|
return (
|
|
2281
3790
|
<TurnSummary
|
|
2282
3791
|
items={activityItems}
|
|
2283
3792
|
outcome={group.outcome}
|
|
2284
3793
|
failureText={insideTurn ? undefined : group.failureText}
|
|
2285
|
-
durationMs={
|
|
3794
|
+
durationMs={durationMs}
|
|
2286
3795
|
defaultOpen={turnDefaultOpen ? true : undefined}
|
|
2287
3796
|
bare={insideTurn}
|
|
2288
3797
|
foldKey={group.id}
|
|
@@ -2307,8 +3816,10 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2307
3816
|
return (
|
|
2308
3817
|
<TimelineRow
|
|
2309
3818
|
item={group.item}
|
|
3819
|
+
renderMessageActions={renderMessageActions}
|
|
2310
3820
|
renderMessageText={renderMessageText}
|
|
2311
3821
|
onReconnect={onReconnect}
|
|
3822
|
+
renderAuthNeeded={renderAuthNeeded}
|
|
2312
3823
|
resolveProviderLogo={resolveProviderLogo}
|
|
2313
3824
|
onOpenSession={onOpenSession}
|
|
2314
3825
|
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
@@ -2317,6 +3828,94 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2317
3828
|
}
|
|
2318
3829
|
});
|
|
2319
3830
|
|
|
3831
|
+
type FoldedGroupBehavior = {
|
|
3832
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
3833
|
+
renderMessageText?:
|
|
3834
|
+
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
3835
|
+
| undefined;
|
|
3836
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3837
|
+
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
3838
|
+
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
3839
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
3840
|
+
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
3841
|
+
toolRegistry: ToolRegistry;
|
|
3842
|
+
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
3843
|
+
loadRetainedArtifact?: RetainedArtifactLoader | undefined;
|
|
3844
|
+
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
3845
|
+
turnSummary?: TurnSummaryOptions | undefined;
|
|
3846
|
+
};
|
|
3847
|
+
|
|
3848
|
+
/**
|
|
3849
|
+
* A search hit inside a nested work row opens that row only. The reveal key
|
|
3850
|
+
* is provided per top-level group, so without narrowing every nested row of
|
|
3851
|
+
* a folded quiet-cycle run would open at once.
|
|
3852
|
+
*/
|
|
3853
|
+
function NestedSearchReveal({ group, children }: { group: TimelineGroup; children: ReactNode }) {
|
|
3854
|
+
const revealKey = useContext(TimelineSearchRevealContext);
|
|
3855
|
+
const itemId = useMemo(() => {
|
|
3856
|
+
if (!revealKey) return null;
|
|
3857
|
+
try {
|
|
3858
|
+
const [id] = JSON.parse(revealKey) as unknown[];
|
|
3859
|
+
return typeof id === "string" ? id : null;
|
|
3860
|
+
} catch {
|
|
3861
|
+
return null;
|
|
3862
|
+
}
|
|
3863
|
+
}, [revealKey]);
|
|
3864
|
+
const value = itemId && timelineGroupItemIds(group).includes(itemId) ? revealKey : null;
|
|
3865
|
+
return (
|
|
3866
|
+
<TimelineSearchRevealContext.Provider value={value}>
|
|
3867
|
+
{children}
|
|
3868
|
+
</TimelineSearchRevealContext.Provider>
|
|
3869
|
+
);
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
function renderFoldedGroups(
|
|
3873
|
+
groups: readonly TimelineGroup[],
|
|
3874
|
+
behavior: FoldedGroupBehavior,
|
|
3875
|
+
readableWork = false,
|
|
3876
|
+
) {
|
|
3877
|
+
// Second-layer chips only when there are natural multi-cluster seams:
|
|
3878
|
+
// otherwise the outer turn chip alone is enough ("N steps" wrapping one
|
|
3879
|
+
// more "N steps" was the redundant double fold).
|
|
3880
|
+
const nestClusters = foldableActivityClusterCount(groups) >= 2;
|
|
3881
|
+
return groups.map((child, index) => (
|
|
3882
|
+
<TimelineGroupRenderBoundary
|
|
3883
|
+
key={timelineGroupKey(child)}
|
|
3884
|
+
resetKeys={[
|
|
3885
|
+
child,
|
|
3886
|
+
behavior.renderMessageActions,
|
|
3887
|
+
behavior.renderMessageText,
|
|
3888
|
+
behavior.onOpenSession,
|
|
3889
|
+
behavior.onMemoryClick,
|
|
3890
|
+
behavior.onReconnect,
|
|
3891
|
+
behavior.renderAuthNeeded,
|
|
3892
|
+
behavior.resolveProviderLogo,
|
|
3893
|
+
behavior.toolRegistry,
|
|
3894
|
+
behavior.loadRetainedScreenshot,
|
|
3895
|
+
behavior.loadRetainedArtifact,
|
|
3896
|
+
behavior.loadVideoArtifactPlayback,
|
|
3897
|
+
behavior.turnSummary,
|
|
3898
|
+
]}
|
|
3899
|
+
>
|
|
3900
|
+
{readableWork ? (
|
|
3901
|
+
<div
|
|
3902
|
+
className={
|
|
3903
|
+
index > 0 && (child.kind !== "activity" || groups[index - 1]?.kind !== "activity")
|
|
3904
|
+
? "mt-3"
|
|
3905
|
+
: undefined
|
|
3906
|
+
}
|
|
3907
|
+
>
|
|
3908
|
+
<NestedSearchReveal group={child}>
|
|
3909
|
+
<TimelineGroupView {...behavior} group={child} insideTurn startupDismissed />
|
|
3910
|
+
</NestedSearchReveal>
|
|
3911
|
+
</div>
|
|
3912
|
+
) : (
|
|
3913
|
+
<TimelineGroupView {...behavior} group={child} insideTurn nestClusterChips={nestClusters} />
|
|
3914
|
+
)}
|
|
3915
|
+
</TimelineGroupRenderBoundary>
|
|
3916
|
+
));
|
|
3917
|
+
}
|
|
3918
|
+
|
|
2320
3919
|
/**
|
|
2321
3920
|
* Body under a turn/activity chip. Remount flashes are gated by the timeline
|
|
2322
3921
|
* seen-activity-id map (not by killing entrance): FoldBody used to force
|
|
@@ -2328,9 +3927,16 @@ function FoldBody({ children }: { children: ReactNode }) {
|
|
|
2328
3927
|
|
|
2329
3928
|
/** Stable left rule for turn/activity bodies — always present so settle wrap
|
|
2330
3929
|
never inserts or removes the rail chrome. */
|
|
2331
|
-
function TurnRailFrame({ children }: { children: ReactNode }) {
|
|
3930
|
+
function TurnRailFrame({ children, compact = false }: { children: ReactNode; compact?: boolean }) {
|
|
2332
3931
|
return (
|
|
2333
|
-
<div
|
|
3932
|
+
<div
|
|
3933
|
+
className={cn(
|
|
3934
|
+
"flex flex-col border-l-2 border-og-border pl-3 sm:pl-4",
|
|
3935
|
+
compact ? "gap-0.5" : "gap-4",
|
|
3936
|
+
)}
|
|
3937
|
+
>
|
|
3938
|
+
{children}
|
|
3939
|
+
</div>
|
|
2334
3940
|
);
|
|
2335
3941
|
}
|
|
2336
3942
|
|
|
@@ -2377,26 +3983,15 @@ function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean {
|
|
|
2377
3983
|
}
|
|
2378
3984
|
}
|
|
2379
3985
|
|
|
2380
|
-
/** Generated images are primary user-visible output, not incidental activity. */
|
|
2381
|
-
function timelineGroupContainsGeneratedImage(group: TimelineGroup): boolean {
|
|
2382
|
-
switch (group.kind) {
|
|
2383
|
-
case "item":
|
|
2384
|
-
return false;
|
|
2385
|
-
case "activity":
|
|
2386
|
-
return group.items.some(
|
|
2387
|
-
(item) => item.kind === "tool-call" && item.name === "generate_image",
|
|
2388
|
-
);
|
|
2389
|
-
case "turn":
|
|
2390
|
-
return group.groups.some(timelineGroupContainsGeneratedImage);
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
3986
|
function timelineGroupItemIds(group: TimelineGroup): string[] {
|
|
2395
3987
|
switch (group.kind) {
|
|
2396
3988
|
case "item":
|
|
2397
3989
|
return [group.item.id];
|
|
2398
3990
|
case "activity":
|
|
2399
|
-
return
|
|
3991
|
+
return [
|
|
3992
|
+
...(group.work?.details ?? []).flatMap(timelineGroupItemIds),
|
|
3993
|
+
...group.items.map((item) => item.id),
|
|
3994
|
+
];
|
|
2400
3995
|
case "turn":
|
|
2401
3996
|
return group.groups.flatMap(timelineGroupItemIds);
|
|
2402
3997
|
}
|
|
@@ -2414,25 +4009,13 @@ function isAgentProgress(next: TimelineGroup | undefined): boolean {
|
|
|
2414
4009
|
return (
|
|
2415
4010
|
next.kind === "activity" ||
|
|
2416
4011
|
next.kind === "turn" ||
|
|
2417
|
-
(next.kind === "item" && next.item.kind === "agent-message")
|
|
4012
|
+
(next.kind === "item" && next.item.kind === "agent-message" && next.item.text.trim().length > 0)
|
|
2418
4013
|
);
|
|
2419
4014
|
}
|
|
2420
4015
|
|
|
2421
4016
|
/** No item still running or streaming — the only state safe to fold live.
|
|
2422
4017
|
Position alone is a broken proxy: a pending queued message (or any trailing
|
|
2423
4018
|
item) can sit after the ACTIVE cluster, which must never fold mid-work. */
|
|
2424
|
-
function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>): boolean {
|
|
2425
|
-
return group.items.every((item) => {
|
|
2426
|
-
if (item.kind === "reasoning") {
|
|
2427
|
-
return !item.streaming;
|
|
2428
|
-
}
|
|
2429
|
-
// Memory writes and fleet observations are discrete, already-settled events.
|
|
2430
|
-
if (item.kind === "memory" || item.kind === "fleet-decision") {
|
|
2431
|
-
return true;
|
|
2432
|
-
}
|
|
2433
|
-
return item.status !== "running";
|
|
2434
|
-
});
|
|
2435
|
-
}
|
|
2436
4019
|
|
|
2437
4020
|
/** Settled activity clusters that could become nested chips under a turn. */
|
|
2438
4021
|
function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number {
|
|
@@ -2445,18 +4028,6 @@ function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number
|
|
|
2445
4028
|
return count;
|
|
2446
4029
|
}
|
|
2447
4030
|
|
|
2448
|
-
function flattenActivityItems(groups: TimelineGroup[]): ActivityItem[] {
|
|
2449
|
-
const items: ActivityItem[] = [];
|
|
2450
|
-
for (const group of groups) {
|
|
2451
|
-
if (group.kind === "activity") {
|
|
2452
|
-
items.push(...group.items);
|
|
2453
|
-
} else if (group.kind === "turn") {
|
|
2454
|
-
items.push(...flattenActivityItems(group.groups));
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
return items;
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
4031
|
/** Assistant prose inside a turn fold (mid-turn narration), joined for copy. */
|
|
2461
4032
|
function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
|
|
2462
4033
|
const parts: string[] = [];
|
|
@@ -2466,6 +4037,18 @@ function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
|
|
|
2466
4037
|
if (text.length > 0) {
|
|
2467
4038
|
parts.push(text);
|
|
2468
4039
|
}
|
|
4040
|
+
} else if (group.kind === "activity") {
|
|
4041
|
+
// Commentary folded into clusters is still the assistant's own prose.
|
|
4042
|
+
const nested = collectAgentMessageText(group.work?.details ?? []);
|
|
4043
|
+
if (nested.length > 0) {
|
|
4044
|
+
parts.push(nested);
|
|
4045
|
+
}
|
|
4046
|
+
for (const item of group.items) {
|
|
4047
|
+
const text = item.kind === "agent-message" ? item.text.trim() : "";
|
|
4048
|
+
if (text.length > 0) {
|
|
4049
|
+
parts.push(text);
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
2469
4052
|
} else if (group.kind === "turn") {
|
|
2470
4053
|
const nested = collectAgentMessageText(group.groups);
|
|
2471
4054
|
if (nested.length > 0) {
|
|
@@ -2486,6 +4069,9 @@ function collectTurnIdsFromGroups(groups: readonly TimelineGroup[]): Set<string>
|
|
|
2486
4069
|
ids.add(turnId);
|
|
2487
4070
|
}
|
|
2488
4071
|
} else if (group.kind === "activity") {
|
|
4072
|
+
for (const nested of collectTurnIdsFromGroups(group.work?.details ?? [])) {
|
|
4073
|
+
ids.add(nested);
|
|
4074
|
+
}
|
|
2489
4075
|
for (const item of group.items) {
|
|
2490
4076
|
if (item.turnId) {
|
|
2491
4077
|
ids.add(item.turnId);
|
|
@@ -2540,15 +4126,6 @@ function collectTurnCopyText(
|
|
|
2540
4126
|
return parts.join("\n\n");
|
|
2541
4127
|
}
|
|
2542
4128
|
|
|
2543
|
-
function durationBetween(startedAt: string, endedAt: string): number | undefined {
|
|
2544
|
-
const started = Date.parse(startedAt);
|
|
2545
|
-
const ended = Date.parse(endedAt);
|
|
2546
|
-
if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
|
|
2547
|
-
return undefined;
|
|
2548
|
-
}
|
|
2549
|
-
return ended - started;
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
4129
|
/* --- single rows ------------------------------------------------------------ */
|
|
2553
4130
|
|
|
2554
4131
|
/**
|
|
@@ -2558,28 +4135,45 @@ function durationBetween(startedAt: string, endedAt: string): number | undefined
|
|
|
2558
4135
|
*/
|
|
2559
4136
|
export function TimelineRow({
|
|
2560
4137
|
item,
|
|
4138
|
+
renderMessageActions,
|
|
2561
4139
|
renderMessageText,
|
|
2562
4140
|
onReconnect,
|
|
4141
|
+
renderAuthNeeded,
|
|
2563
4142
|
resolveProviderLogo,
|
|
2564
4143
|
onOpenSession,
|
|
2565
4144
|
loadVideoArtifactPlayback,
|
|
2566
4145
|
}: {
|
|
2567
4146
|
item: TimelineItem;
|
|
4147
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2568
4148
|
renderMessageText?:
|
|
2569
4149
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2570
4150
|
| undefined;
|
|
2571
4151
|
onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
|
|
4152
|
+
/** Host-owned inline connection setup. Return undefined to use the default recovery card. */
|
|
4153
|
+
renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
|
|
2572
4154
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2573
4155
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2574
4156
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
2575
4157
|
}) {
|
|
2576
4158
|
switch (item.kind) {
|
|
2577
4159
|
case "user-message":
|
|
2578
|
-
return
|
|
4160
|
+
return (
|
|
4161
|
+
<UserMessageRow
|
|
4162
|
+
item={item}
|
|
4163
|
+
renderMessageActions={renderMessageActions}
|
|
4164
|
+
renderMessageText={renderMessageText}
|
|
4165
|
+
/>
|
|
4166
|
+
);
|
|
2579
4167
|
case "human-input":
|
|
2580
4168
|
return <HumanInputConversationRow item={item} />;
|
|
2581
4169
|
case "agent-message":
|
|
2582
|
-
return
|
|
4170
|
+
return (
|
|
4171
|
+
<AgentMessageRow
|
|
4172
|
+
item={item}
|
|
4173
|
+
renderMessageActions={renderMessageActions}
|
|
4174
|
+
renderMessageText={renderMessageText}
|
|
4175
|
+
/>
|
|
4176
|
+
);
|
|
2583
4177
|
case "worker-completion":
|
|
2584
4178
|
return <WorkerCompletionRow item={item} onOpenSession={onOpenSession} />;
|
|
2585
4179
|
case "session-status":
|
|
@@ -2588,7 +4182,11 @@ export function TimelineRow({
|
|
|
2588
4182
|
return <GoalRow item={item} />;
|
|
2589
4183
|
case "machine-input-batch":
|
|
2590
4184
|
return (
|
|
2591
|
-
<MachineInputBatchRow
|
|
4185
|
+
<MachineInputBatchRow
|
|
4186
|
+
item={item}
|
|
4187
|
+
onOpenSession={onOpenSession}
|
|
4188
|
+
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
4189
|
+
/>
|
|
2592
4190
|
);
|
|
2593
4191
|
case "notice":
|
|
2594
4192
|
return <NoticeRow item={item} />;
|
|
@@ -2596,11 +4194,13 @@ export function TimelineRow({
|
|
|
2596
4194
|
return <CompactionRow item={item} />;
|
|
2597
4195
|
case "auth-needed":
|
|
2598
4196
|
return (
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
4197
|
+
renderAuthNeeded?.(item) ?? (
|
|
4198
|
+
<AuthNeededRow
|
|
4199
|
+
item={item}
|
|
4200
|
+
onReconnect={onReconnect}
|
|
4201
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
4202
|
+
/>
|
|
4203
|
+
)
|
|
2604
4204
|
);
|
|
2605
4205
|
default:
|
|
2606
4206
|
return null;
|
|
@@ -2616,6 +4216,8 @@ const COMPACTION_TRIGGER_LABEL: Record<NonNullable<ContextCompactionItem["trigge
|
|
|
2616
4216
|
|
|
2617
4217
|
function CompactionRow({ item }: { item: ContextCompactionItem }) {
|
|
2618
4218
|
const enter = useEntranceAnimation();
|
|
4219
|
+
const detailsId = useId();
|
|
4220
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
2619
4221
|
const trigger =
|
|
2620
4222
|
item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL[item.trigger] : null;
|
|
2621
4223
|
const before =
|
|
@@ -2633,56 +4235,97 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
|
|
|
2633
4235
|
? before && after
|
|
2634
4236
|
? `Conversation history compacted · ~${before} → ~${after} estimated history tokens`
|
|
2635
4237
|
: "Conversation history compacted"
|
|
2636
|
-
:
|
|
4238
|
+
: compactionSkipTitle(item.skipReason);
|
|
2637
4239
|
const subtitle =
|
|
2638
4240
|
item.phase === "compacted"
|
|
2639
4241
|
? "Chat history above is unchanged"
|
|
2640
4242
|
: item.phase === "skipped"
|
|
2641
|
-
? compactionSkipSubtitle(item.skipReason)
|
|
4243
|
+
? compactionSkipSubtitle(item.skipReason, item.providerRejection)
|
|
2642
4244
|
: null;
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
4245
|
+
// Provider identifiers are diagnostics: one short line up front, the rest on demand.
|
|
4246
|
+
const details = compactionFailureDetails(item);
|
|
4247
|
+
const failed = item.phase === "skipped" && item.skipReason === "summarization_failed";
|
|
4248
|
+
const pill = failed
|
|
4249
|
+
? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
|
|
4250
|
+
: item.phase === "started"
|
|
4251
|
+
? WAITING_PILL_CLASS
|
|
4252
|
+
: NEUTRAL_PILL;
|
|
2649
4253
|
return (
|
|
2650
|
-
<div className={cn(enter && "animate-og-enter", "flex
|
|
4254
|
+
<div className={cn(enter && "animate-og-enter", "flex min-w-0 flex-col items-center gap-1.5")}>
|
|
2651
4255
|
<div
|
|
2652
4256
|
className={cn(
|
|
2653
|
-
"inline-flex max-w-full flex-col items-center gap-0.5
|
|
4257
|
+
"inline-flex min-w-0 max-w-full flex-col items-center gap-0.5 border px-3 py-1.5 text-og-sm",
|
|
4258
|
+
// A failure's recovery step may wrap on narrow screens; a full pill
|
|
4259
|
+
// radius would pinch its second line.
|
|
4260
|
+
failed ? "rounded-og-lg" : "rounded-full",
|
|
2654
4261
|
pill,
|
|
2655
4262
|
)}
|
|
2656
4263
|
role="status"
|
|
2657
4264
|
>
|
|
2658
|
-
<span className="inline-flex max-w-full items-center gap-1.5">
|
|
4265
|
+
<span className="inline-flex min-w-0 max-w-full items-center gap-1.5">
|
|
2659
4266
|
<ShrinkIcon className="size-3.5 shrink-0" />
|
|
2660
|
-
<span className="truncate">
|
|
4267
|
+
<span className="min-w-0 truncate">
|
|
2661
4268
|
{title}
|
|
2662
4269
|
{trigger ? ` · ${trigger}` : ""}
|
|
2663
4270
|
</span>
|
|
2664
4271
|
</span>
|
|
2665
|
-
{subtitle
|
|
4272
|
+
{subtitle || details ? (
|
|
4273
|
+
<span
|
|
4274
|
+
className={cn(
|
|
4275
|
+
"min-w-0 max-w-full text-og-xs",
|
|
4276
|
+
failed ? "text-balance text-center" : "inline-flex items-center gap-1",
|
|
4277
|
+
)}
|
|
4278
|
+
>
|
|
4279
|
+
{subtitle ? (
|
|
4280
|
+
<span className={cn("opacity-80", !failed && "min-w-0 truncate")}>{subtitle}</span>
|
|
4281
|
+
) : null}
|
|
4282
|
+
{subtitle && details && failed ? " " : null}
|
|
4283
|
+
{details ? (
|
|
4284
|
+
<button
|
|
4285
|
+
type="button"
|
|
4286
|
+
aria-expanded={detailsOpen}
|
|
4287
|
+
aria-controls={detailsId}
|
|
4288
|
+
onClick={() => setDetailsOpen((open) => !open)}
|
|
4289
|
+
className={cn(
|
|
4290
|
+
"inline-flex shrink-0 items-center gap-0.5 whitespace-nowrap rounded-og-sm align-baseline font-medium underline-offset-2",
|
|
4291
|
+
"hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-current/40",
|
|
4292
|
+
"pointer-coarse:-my-2 pointer-coarse:px-1 pointer-coarse:py-2",
|
|
4293
|
+
)}
|
|
4294
|
+
>
|
|
4295
|
+
Details
|
|
4296
|
+
<ChevronDownIcon
|
|
4297
|
+
aria-hidden
|
|
4298
|
+
className={cn("size-3 transition-transform", detailsOpen && "rotate-180")}
|
|
4299
|
+
/>
|
|
4300
|
+
</button>
|
|
4301
|
+
) : null}
|
|
4302
|
+
</span>
|
|
4303
|
+
) : null}
|
|
2666
4304
|
</div>
|
|
4305
|
+
{details ? (
|
|
4306
|
+
<div
|
|
4307
|
+
id={detailsId}
|
|
4308
|
+
hidden={!detailsOpen}
|
|
4309
|
+
data-og-compaction-details=""
|
|
4310
|
+
className="w-full max-w-md rounded-og-md border border-og-border bg-og-surface-1 px-3 py-2 text-left text-og-xs text-og-fg-muted"
|
|
4311
|
+
>
|
|
4312
|
+
{details.note ? <p className="mb-2">{details.note}</p> : null}
|
|
4313
|
+
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1">
|
|
4314
|
+
{details.facts.map((fact) => (
|
|
4315
|
+
<Fragment key={fact.label}>
|
|
4316
|
+
<dt className="text-og-fg-subtle">{fact.label}</dt>
|
|
4317
|
+
<dd className="max-h-40 min-w-0 overflow-auto whitespace-pre-wrap break-words font-mono">
|
|
4318
|
+
{fact.value}
|
|
4319
|
+
</dd>
|
|
4320
|
+
</Fragment>
|
|
4321
|
+
))}
|
|
4322
|
+
</dl>
|
|
4323
|
+
</div>
|
|
4324
|
+
) : null}
|
|
2667
4325
|
</div>
|
|
2668
4326
|
);
|
|
2669
4327
|
}
|
|
2670
4328
|
|
|
2671
|
-
function compactionSkipSubtitle(reason: string | null): string {
|
|
2672
|
-
switch (reason) {
|
|
2673
|
-
case "no_history":
|
|
2674
|
-
return "No active history to compact";
|
|
2675
|
-
case "replacement_not_smaller":
|
|
2676
|
-
return "Checkpoint would not reduce memory size";
|
|
2677
|
-
case "replacement_unchanged":
|
|
2678
|
-
return "Checkpoint made no progress";
|
|
2679
|
-
case "summarization_failed":
|
|
2680
|
-
return "Request it again to retry. Chat history is unchanged.";
|
|
2681
|
-
default:
|
|
2682
|
-
return "Compaction was not needed. Chat history is unchanged.";
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
4329
|
/** Hover-reveal clock beside the copy control (sent / finished). */
|
|
2687
4330
|
function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
|
|
2688
4331
|
return (
|
|
@@ -2701,9 +4344,11 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
|
|
|
2701
4344
|
|
|
2702
4345
|
function UserMessageRow({
|
|
2703
4346
|
item,
|
|
4347
|
+
renderMessageActions,
|
|
2704
4348
|
renderMessageText,
|
|
2705
4349
|
}: {
|
|
2706
4350
|
item: UserMessageItem;
|
|
4351
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2707
4352
|
renderMessageText?:
|
|
2708
4353
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2709
4354
|
| undefined;
|
|
@@ -2713,6 +4358,7 @@ function UserMessageRow({
|
|
|
2713
4358
|
return (
|
|
2714
4359
|
<div className={cn(enter && "animate-og-enter", "flex justify-end")}>
|
|
2715
4360
|
<div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
|
|
4361
|
+
<MessageSenderSlot item={item} />
|
|
2716
4362
|
<CopyHoverFrame
|
|
2717
4363
|
copyText={
|
|
2718
4364
|
item.text ||
|
|
@@ -2722,22 +4368,30 @@ function UserMessageRow({
|
|
|
2722
4368
|
}
|
|
2723
4369
|
label="Copy message"
|
|
2724
4370
|
className="w-fit max-w-full min-w-0"
|
|
2725
|
-
trailing={
|
|
4371
|
+
trailing={
|
|
4372
|
+
<>
|
|
4373
|
+
{renderMessageActions?.(item)}
|
|
4374
|
+
<MessageFooterTime occurredAt={item.occurredAt} />
|
|
4375
|
+
</>
|
|
4376
|
+
}
|
|
2726
4377
|
>
|
|
2727
4378
|
<div className={MESSAGE_BUBBLE_CLASS}>
|
|
2728
4379
|
{item.text ? (
|
|
2729
|
-
<div
|
|
4380
|
+
<div
|
|
4381
|
+
data-og-search-item={item.id}
|
|
4382
|
+
data-og-annotation-source-key={item.annotationSource?.eventId}
|
|
4383
|
+
>
|
|
2730
4384
|
{renderMessageText ? (
|
|
2731
4385
|
renderMessageText(item.text, item)
|
|
2732
4386
|
) : (
|
|
2733
4387
|
<UserMessageBody messageId={item.id} text={item.text}>
|
|
2734
|
-
<Markdown>{item.text}</Markdown>
|
|
4388
|
+
<Markdown softLineBreaks>{item.text}</Markdown>
|
|
2735
4389
|
</UserMessageBody>
|
|
2736
4390
|
)}
|
|
2737
4391
|
</div>
|
|
2738
4392
|
) : null}
|
|
2739
4393
|
{(item.annotations?.length ?? 0) > 0 ? (
|
|
2740
|
-
<
|
|
4394
|
+
<TimelineAnnotationCards
|
|
2741
4395
|
annotations={item.annotations ?? []}
|
|
2742
4396
|
className={item.text ? "mt-2" : undefined}
|
|
2743
4397
|
/>
|
|
@@ -2747,30 +4401,43 @@ function UserMessageRow({
|
|
|
2747
4401
|
{deliveryFailed ? (
|
|
2748
4402
|
<div
|
|
2749
4403
|
role="status"
|
|
2750
|
-
className="flex max-w-full items-
|
|
4404
|
+
className="flex max-w-full flex-col items-start gap-1 px-1 text-og-xs text-og-status-failed"
|
|
2751
4405
|
>
|
|
2752
|
-
<span className="
|
|
4406
|
+
<span className="flex min-w-0 items-start gap-1">
|
|
2753
4407
|
<TriangleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
2754
|
-
<span
|
|
4408
|
+
<span className="min-w-0 break-words">
|
|
4409
|
+
{item.delivery?.error || "Message not sent"}
|
|
4410
|
+
</span>
|
|
2755
4411
|
</span>
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
4412
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
4413
|
+
{item.delivery?.onEdit ? (
|
|
4414
|
+
<button
|
|
4415
|
+
type="button"
|
|
4416
|
+
className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
|
|
4417
|
+
onClick={item.delivery.onEdit}
|
|
4418
|
+
>
|
|
4419
|
+
Edit message
|
|
4420
|
+
</button>
|
|
4421
|
+
) : null}
|
|
4422
|
+
{item.delivery?.onRetry ? (
|
|
4423
|
+
<button
|
|
4424
|
+
type="button"
|
|
4425
|
+
className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
|
|
4426
|
+
onClick={item.delivery.onRetry}
|
|
4427
|
+
>
|
|
4428
|
+
Retry
|
|
4429
|
+
</button>
|
|
4430
|
+
) : null}
|
|
4431
|
+
{item.delivery?.onRemove ? (
|
|
4432
|
+
<button
|
|
4433
|
+
type="button"
|
|
4434
|
+
className="font-medium text-og-fg-subtle underline decoration-og-border underline-offset-2 hover:text-og-fg"
|
|
4435
|
+
onClick={item.delivery.onRemove}
|
|
4436
|
+
>
|
|
4437
|
+
Remove
|
|
4438
|
+
</button>
|
|
4439
|
+
) : null}
|
|
4440
|
+
</div>
|
|
2774
4441
|
</div>
|
|
2775
4442
|
) : null}
|
|
2776
4443
|
</div>
|
|
@@ -2810,7 +4477,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2810
4477
|
item.questions.map((question, index) => (
|
|
2811
4478
|
<div key={question.id}>
|
|
2812
4479
|
{question.label ? (
|
|
2813
|
-
<p className="text-og-sm font-semibold text-og-fg">
|
|
4480
|
+
<p className="text-og-sm font-semibold text-og-fg [overflow-wrap:anywhere]">
|
|
2814
4481
|
{multipleQuestions ? (
|
|
2815
4482
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
2816
4483
|
) : null}
|
|
@@ -2820,7 +4487,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2820
4487
|
) : null}
|
|
2821
4488
|
<p
|
|
2822
4489
|
className={cn(
|
|
2823
|
-
"text-og-md leading-6 text-og-fg",
|
|
4490
|
+
"text-og-md leading-6 text-og-fg [overflow-wrap:anywhere]",
|
|
2824
4491
|
question.label && "mt-0.5 text-og-sm text-og-fg-muted",
|
|
2825
4492
|
)}
|
|
2826
4493
|
>
|
|
@@ -2828,7 +4495,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2828
4495
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
2829
4496
|
) : null}
|
|
2830
4497
|
{!question.label && multipleQuestions ? " " : null}
|
|
2831
|
-
{question.prompt}
|
|
4498
|
+
{linkifyText(question.prompt)}
|
|
2832
4499
|
</p>
|
|
2833
4500
|
</div>
|
|
2834
4501
|
))
|
|
@@ -2862,7 +4529,12 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2862
4529
|
</p>
|
|
2863
4530
|
) : null}
|
|
2864
4531
|
<div className={cn("text-og-md text-og-fg", multipleQuestions && "mt-0.5")}>
|
|
2865
|
-
{answer.
|
|
4532
|
+
{answer.secret ? (
|
|
4533
|
+
<SecretAnswerChip
|
|
4534
|
+
result={answer.secret}
|
|
4535
|
+
label={secretAnswerLabel(answer.secret)}
|
|
4536
|
+
/>
|
|
4537
|
+
) : answer.values.length > 1 ? (
|
|
2866
4538
|
<ul className="list-disc space-y-0.5 pl-5">
|
|
2867
4539
|
{answer.values.map((value) => (
|
|
2868
4540
|
<li key={`${answer.questionId}-${value}`}>{value}</li>
|
|
@@ -2903,7 +4575,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
|
|
|
2903
4575
|
item.questions.length > 1
|
|
2904
4576
|
? `${questionNumberById.get(answer.questionId) ?? index + 1}. `
|
|
2905
4577
|
: ""
|
|
2906
|
-
}${answer.label}: ${
|
|
4578
|
+
}${answer.label}: ${
|
|
4579
|
+
answer.secret
|
|
4580
|
+
? `${secretAnswerLabel(answer.secret)} (${answer.secret.reference})`
|
|
4581
|
+
: answer.values.join(", ") || "Answered"
|
|
4582
|
+
}`,
|
|
2907
4583
|
)
|
|
2908
4584
|
.join("\n\n");
|
|
2909
4585
|
return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`]
|
|
@@ -2913,9 +4589,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
|
|
|
2913
4589
|
|
|
2914
4590
|
function AgentMessageRow({
|
|
2915
4591
|
item,
|
|
4592
|
+
renderMessageActions,
|
|
2916
4593
|
renderMessageText,
|
|
2917
4594
|
}: {
|
|
2918
4595
|
item: AgentMessageItem;
|
|
4596
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2919
4597
|
renderMessageText?:
|
|
2920
4598
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2921
4599
|
| undefined;
|
|
@@ -2926,76 +4604,80 @@ function AgentMessageRow({
|
|
|
2926
4604
|
const body = renderMessageText ? (
|
|
2927
4605
|
renderMessageText(item.text, item)
|
|
2928
4606
|
) : (
|
|
2929
|
-
<Markdown streaming={item.streaming}>
|
|
4607
|
+
<Markdown softLineBreaks streaming={item.streaming}>
|
|
4608
|
+
{item.text}
|
|
4609
|
+
</Markdown>
|
|
2930
4610
|
);
|
|
2931
4611
|
// While streaming, copy is still useful (current text) but keep chrome calm —
|
|
2932
4612
|
// stamp only after the message finishes (occurredAt tracks completion).
|
|
2933
4613
|
return (
|
|
2934
|
-
<
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
4614
|
+
<CopyHoverFrame
|
|
4615
|
+
copyText={item.text}
|
|
4616
|
+
label="Copy message"
|
|
4617
|
+
align="start"
|
|
4618
|
+
className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
|
|
4619
|
+
trailing={
|
|
4620
|
+
item.streaming ? null : (
|
|
4621
|
+
<>
|
|
4622
|
+
{renderMessageActions?.(item)}
|
|
4623
|
+
<MessageFooterTime occurredAt={item.occurredAt} />
|
|
4624
|
+
</>
|
|
4625
|
+
)
|
|
4626
|
+
}
|
|
4627
|
+
>
|
|
4628
|
+
<div
|
|
4629
|
+
data-og-wide-table-message=""
|
|
4630
|
+
data-og-search-item={item.id}
|
|
4631
|
+
data-og-annotation-source-key={item.annotationSource?.eventId}
|
|
2941
4632
|
>
|
|
2942
4633
|
{body}
|
|
2943
|
-
</
|
|
2944
|
-
</
|
|
4634
|
+
</div>
|
|
4635
|
+
</CopyHoverFrame>
|
|
2945
4636
|
);
|
|
2946
4637
|
}
|
|
2947
4638
|
|
|
2948
4639
|
/**
|
|
2949
|
-
* A worker session reporting back to its manager
|
|
2950
|
-
* as a `user.message` carrying a `childCompletion` payload
|
|
2951
|
-
*
|
|
2952
|
-
*
|
|
2953
|
-
*
|
|
2954
|
-
* full report, evidence, and any paused reason live behind a collapsed
|
|
2955
|
-
* disclosure, and a "View session" affordance deep-links into the child.
|
|
2956
|
-
*
|
|
2957
|
-
* Color follows the timeline's restraint: green only for a completed goal, the
|
|
2958
|
-
* waiting hue only for a paused one, red only for a failed child — everything
|
|
2959
|
-
* else is a neutral inbound card.
|
|
4640
|
+
* A worker session reporting back to its manager in older history: the child's
|
|
4641
|
+
* completion arrived as a `user.message` carrying a `childCompletion` payload.
|
|
4642
|
+
* It draws as the same named agent row as every other inbound agent update:
|
|
4643
|
+
* the outcome and goal on the row, the report, evidence, and any paused
|
|
4644
|
+
* reason behind the disclosure, and a deep link into the child.
|
|
2960
4645
|
*/
|
|
2961
4646
|
type WorkerCompletionMeta = {
|
|
2962
|
-
|
|
4647
|
+
title: AgentTitleParts;
|
|
2963
4648
|
icon: ComponentType<{ className?: string }>;
|
|
2964
|
-
|
|
2965
|
-
/** The 2px left-accent hue — color spent only on the exceptional outcomes. */
|
|
2966
|
-
accentClass: string;
|
|
4649
|
+
failed: boolean;
|
|
2967
4650
|
};
|
|
2968
4651
|
|
|
2969
|
-
function workerCompletionMeta(
|
|
4652
|
+
function workerCompletionMeta(
|
|
4653
|
+
item: WorkerCompletionItem,
|
|
4654
|
+
name: string | null,
|
|
4655
|
+
): WorkerCompletionMeta {
|
|
2970
4656
|
if (item.childStatus === "failed") {
|
|
2971
4657
|
return {
|
|
2972
|
-
|
|
4658
|
+
title: namedAgentTitle(name, "", " failed", "Worker failed"),
|
|
2973
4659
|
icon: XCircleIcon,
|
|
2974
|
-
|
|
2975
|
-
accentClass: "border-og-status-failed/60",
|
|
4660
|
+
failed: true,
|
|
2976
4661
|
};
|
|
2977
4662
|
}
|
|
2978
4663
|
if (item.goalStatus === "paused") {
|
|
2979
4664
|
return {
|
|
2980
|
-
|
|
4665
|
+
title: namedAgentTitle(name, "", " paused", "Worker paused"),
|
|
2981
4666
|
icon: PauseCircleIcon,
|
|
2982
|
-
|
|
2983
|
-
accentClass: "border-og-status-waiting/50",
|
|
4667
|
+
failed: false,
|
|
2984
4668
|
};
|
|
2985
4669
|
}
|
|
2986
4670
|
if (item.goalStatus === "completed") {
|
|
2987
4671
|
return {
|
|
2988
|
-
|
|
4672
|
+
title: namedAgentTitle(name, "", " completed its goal", "Worker completed"),
|
|
2989
4673
|
icon: CheckCircle2Icon,
|
|
2990
|
-
|
|
2991
|
-
accentClass: "border-og-status-idle/45",
|
|
4674
|
+
failed: false,
|
|
2992
4675
|
};
|
|
2993
4676
|
}
|
|
2994
4677
|
return {
|
|
2995
|
-
|
|
4678
|
+
title: namedAgentTitle(name, "Result from ", "", "Worker reported back"),
|
|
2996
4679
|
icon: BotIcon,
|
|
2997
|
-
|
|
2998
|
-
accentClass: "border-og-border-strong",
|
|
4680
|
+
failed: false,
|
|
2999
4681
|
};
|
|
3000
4682
|
}
|
|
3001
4683
|
|
|
@@ -3007,14 +4689,12 @@ function WorkerCompletionRow({
|
|
|
3007
4689
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3008
4690
|
}) {
|
|
3009
4691
|
const enter = useEntranceAnimation();
|
|
3010
|
-
const
|
|
3011
|
-
const meta = workerCompletionMeta(item);
|
|
4692
|
+
const identity = useAgentIdentity();
|
|
4693
|
+
const meta = workerCompletionMeta(item, identity.titleFor(item.childSessionId));
|
|
3012
4694
|
const Icon = meta.icon;
|
|
3013
|
-
// The worker's own report is the substance behind the fold; evidence and any
|
|
3014
|
-
// paused reason sit alongside it as quieter, labelled context.
|
|
3015
4695
|
// "Paused because" only when the outcome actually IS a pause — completion
|
|
3016
4696
|
// payloads can carry a leftover pausedReason/rationale from earlier in the
|
|
3017
|
-
// worker's life, and a
|
|
4697
|
+
// worker's life, and a completed row must not show a pause section.
|
|
3018
4698
|
const showPausedReason =
|
|
3019
4699
|
item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim();
|
|
3020
4700
|
const details: { label: string; value: string; muted?: boolean }[] = [
|
|
@@ -3026,82 +4706,44 @@ function WorkerCompletionRow({
|
|
|
3026
4706
|
? [{ label: "Paused because", value: item.pausedReason!.trim(), muted: true }]
|
|
3027
4707
|
: []),
|
|
3028
4708
|
];
|
|
3029
|
-
const hasDetails = details.length > 0;
|
|
3030
4709
|
return (
|
|
3031
4710
|
<div className={cn(enter && "animate-og-enter", "min-w-0")}>
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
className={cn(
|
|
3052
|
-
"-my-0.5 -mr-1 inline-flex shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-sm font-medium text-og-fg-muted pointer-coarse:py-2",
|
|
3053
|
-
"outline-hidden transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
|
|
3054
|
-
"focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
3055
|
-
)}
|
|
3056
|
-
>
|
|
3057
|
-
View session
|
|
3058
|
-
<ArrowRightIcon className="size-3.5" />
|
|
3059
|
-
</button>
|
|
3060
|
-
) : null}
|
|
3061
|
-
</div>
|
|
3062
|
-
{hasDetails ? (
|
|
3063
|
-
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
|
3064
|
-
<Collapsible.Trigger asChild>
|
|
3065
|
-
<button
|
|
3066
|
-
type="button"
|
|
3067
|
-
className={cn(
|
|
3068
|
-
"group/wc -mx-1 inline-flex w-fit items-center gap-1 rounded-og-sm px-1 py-0.5 text-og-xs font-medium text-og-fg-subtle",
|
|
3069
|
-
"outline-hidden transition-colors duration-150 hover:text-og-fg-muted focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
3070
|
-
)}
|
|
3071
|
-
>
|
|
3072
|
-
<ChevronRightIcon className="size-3 transition-transform duration-150 ease-og-in-out group-data-[state=open]/wc:rotate-90" />
|
|
3073
|
-
{open ? "Hide details" : "Show details"}
|
|
3074
|
-
</button>
|
|
3075
|
-
</Collapsible.Trigger>
|
|
3076
|
-
<Collapsible.Content className="overflow-hidden data-[state=closed]:animate-og-collapse data-[state=open]:animate-og-expand">
|
|
3077
|
-
<div className="ml-1 mt-1.5 flex flex-col gap-2.5">
|
|
3078
|
-
{details.map((detail) => (
|
|
3079
|
-
<div key={detail.label} className="min-w-0">
|
|
3080
|
-
<p className="mb-1 text-og-xs font-medium uppercase tracking-[0.08em] text-og-fg-subtle">
|
|
3081
|
-
{detail.label}
|
|
3082
|
-
</p>
|
|
3083
|
-
<p
|
|
3084
|
-
className={cn(
|
|
3085
|
-
"whitespace-pre-wrap break-words text-og-sm leading-6",
|
|
3086
|
-
detail.muted ? "text-og-fg-subtle" : "text-og-fg-muted",
|
|
3087
|
-
)}
|
|
3088
|
-
>
|
|
3089
|
-
{detail.value}
|
|
3090
|
-
</p>
|
|
3091
|
-
</div>
|
|
3092
|
-
))}
|
|
3093
|
-
</div>
|
|
3094
|
-
</Collapsible.Content>
|
|
3095
|
-
</Collapsible.Root>
|
|
3096
|
-
) : null}
|
|
3097
|
-
</div>
|
|
4711
|
+
<AgentRow
|
|
4712
|
+
icon={<Icon />}
|
|
4713
|
+
title={meta.title}
|
|
4714
|
+
preview={item.goalText ? truncate(item.goalText, 240) : null}
|
|
4715
|
+
previewLines={2}
|
|
4716
|
+
sessionId={item.childSessionId}
|
|
4717
|
+
onOpenSession={onOpenSession ?? identity.onOpenSession}
|
|
4718
|
+
failed={meta.failed}
|
|
4719
|
+
>
|
|
4720
|
+
{details.length > 0 ? (
|
|
4721
|
+
<>
|
|
4722
|
+
{details.map((detail) => (
|
|
4723
|
+
<AgentRowSection key={detail.label} label={detail.label} muted={detail.muted}>
|
|
4724
|
+
{detail.value}
|
|
4725
|
+
</AgentRowSection>
|
|
4726
|
+
))}
|
|
4727
|
+
</>
|
|
4728
|
+
) : undefined}
|
|
4729
|
+
</AgentRow>
|
|
3098
4730
|
</div>
|
|
3099
4731
|
);
|
|
3100
4732
|
}
|
|
3101
4733
|
|
|
3102
|
-
function SessionStatusRow({
|
|
4734
|
+
function SessionStatusRow({
|
|
4735
|
+
item,
|
|
4736
|
+
}: {
|
|
4737
|
+
item: {
|
|
4738
|
+
status: SessionStatus;
|
|
4739
|
+
blocked?: true;
|
|
4740
|
+
occurredAt: string;
|
|
4741
|
+
resolvedAt?: string;
|
|
4742
|
+
};
|
|
4743
|
+
}) {
|
|
3103
4744
|
const enter = useEntranceAnimation();
|
|
3104
|
-
const
|
|
4745
|
+
const status = item.blocked ? "blocked" : item.status;
|
|
4746
|
+
const meta = SESSION_STATUS_META[status];
|
|
3105
4747
|
return (
|
|
3106
4748
|
<div
|
|
3107
4749
|
className={cn(
|
|
@@ -3112,8 +4754,9 @@ function SessionStatusRow({ item }: { item: { status: SessionStatus; occurredAt:
|
|
|
3112
4754
|
>
|
|
3113
4755
|
<span className="h-px flex-1 bg-og-border" />
|
|
3114
4756
|
<span className="inline-flex items-center gap-1.5">
|
|
3115
|
-
<StatusDot status={
|
|
3116
|
-
{meta.label.toLowerCase()} ·
|
|
4757
|
+
{item.resolvedAt ? null : <StatusDot status={status} className="size-1" />}
|
|
4758
|
+
{item.resolvedAt ? "work resumed" : meta.label.toLowerCase()} ·{" "}
|
|
4759
|
+
{formatRelativeTime(item.resolvedAt ?? item.occurredAt)}
|
|
3117
4760
|
</span>
|
|
3118
4761
|
<span className="h-px flex-1 bg-og-border" />
|
|
3119
4762
|
</div>
|
|
@@ -3141,41 +4784,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
|
|
|
3141
4784
|
|
|
3142
4785
|
const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
|
|
3143
4786
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
label: "Goal
|
|
3165
|
-
pill:
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
4787
|
+
// Shared production chunks can be cyclic. Resolve icon bindings during render,
|
|
4788
|
+
// after their modules initialize, rather than permanently capturing undefined.
|
|
4789
|
+
function goalMeta(action: GoalItem["action"]): GoalMeta {
|
|
4790
|
+
const metadata: Record<GoalItem["action"], GoalMeta> = {
|
|
4791
|
+
set: {
|
|
4792
|
+
label: "Goal set",
|
|
4793
|
+
pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
|
|
4794
|
+
icon: TargetIcon,
|
|
4795
|
+
},
|
|
4796
|
+
updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
|
|
4797
|
+
completed: {
|
|
4798
|
+
label: "Goal completed",
|
|
4799
|
+
pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
|
|
4800
|
+
icon: CheckIcon,
|
|
4801
|
+
},
|
|
4802
|
+
paused: {
|
|
4803
|
+
label: "Goal paused",
|
|
4804
|
+
pill: WAITING_PILL_CLASS,
|
|
4805
|
+
icon: PauseIcon,
|
|
4806
|
+
},
|
|
4807
|
+
resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
|
|
4808
|
+
cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
|
|
4809
|
+
held: {
|
|
4810
|
+
label: "Goal held",
|
|
4811
|
+
pill: WAITING_PILL_CLASS,
|
|
4812
|
+
icon: PauseCircleIcon,
|
|
4813
|
+
},
|
|
4814
|
+
continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
|
|
4815
|
+
};
|
|
4816
|
+
return metadata[action];
|
|
4817
|
+
}
|
|
3170
4818
|
|
|
3171
4819
|
/**
|
|
3172
4820
|
* A goal landmark pill. Resolves its label, accent/tone, and glyph from
|
|
3173
|
-
* {@link
|
|
4821
|
+
* {@link goalMeta} so all six actions are visually distinguishable while the
|
|
3174
4822
|
* palette stays restrained — see that table for the per-action rationale.
|
|
3175
4823
|
*/
|
|
3176
4824
|
function GoalRow({ item }: { item: GoalItem }) {
|
|
3177
4825
|
const enter = useEntranceAnimation();
|
|
3178
|
-
const { label, pill, icon: Icon } =
|
|
4826
|
+
const { label, pill, icon: Icon } = goalMeta(item.action);
|
|
3179
4827
|
return (
|
|
3180
4828
|
<div className={cn(enter && "animate-og-enter", "flex justify-center")}>
|
|
3181
4829
|
<span
|
|
@@ -3196,13 +4844,16 @@ function GoalRow({ item }: { item: GoalItem }) {
|
|
|
3196
4844
|
|
|
3197
4845
|
function MachineInputBatchRow({
|
|
3198
4846
|
item,
|
|
4847
|
+
onOpenSession,
|
|
3199
4848
|
loadVideoArtifactPlayback,
|
|
3200
4849
|
}: {
|
|
3201
4850
|
item: MachineInputBatchItem;
|
|
4851
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3202
4852
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
3203
4853
|
}) {
|
|
3204
4854
|
const enter = useEntranceAnimation();
|
|
3205
|
-
const
|
|
4855
|
+
const identity = useAgentIdentity();
|
|
4856
|
+
const label = machineInputBatchLabel(item.members, identity.titleFor);
|
|
3206
4857
|
const single = item.members.length === 1 ? item.members[0]! : null;
|
|
3207
4858
|
if (single?.kind === "media_generation_result" && single.result) {
|
|
3208
4859
|
return (
|
|
@@ -3213,12 +4864,21 @@ function MachineInputBatchRow({
|
|
|
3213
4864
|
);
|
|
3214
4865
|
}
|
|
3215
4866
|
const singleSummary = single ? cleanMachineInputSummary(single.summary) : "";
|
|
4867
|
+
// An agent's words live in its named row behind the pill; the pill already
|
|
4868
|
+
// says who it is from, so nothing repeats beneath it.
|
|
3216
4869
|
const showCollapsedSummary =
|
|
3217
|
-
|
|
4870
|
+
!item.compact &&
|
|
4871
|
+
single != null &&
|
|
4872
|
+
!isAgentMember(single) &&
|
|
4873
|
+
machineInputSummaryIsUseful(single.kind, singleSummary);
|
|
3218
4874
|
|
|
3219
4875
|
return (
|
|
3220
4876
|
<div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
|
|
3221
|
-
<details
|
|
4877
|
+
<details
|
|
4878
|
+
className="group w-full max-w-full"
|
|
4879
|
+
data-og-machine-input-batch=""
|
|
4880
|
+
title={`Received ${formatClockTime(item.occurredAt)}`}
|
|
4881
|
+
>
|
|
3222
4882
|
<summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
|
|
3223
4883
|
<span
|
|
3224
4884
|
className={cn(
|
|
@@ -3233,14 +4893,26 @@ function MachineInputBatchRow({
|
|
|
3233
4893
|
<span className="truncate">{label}</span>
|
|
3234
4894
|
</span>
|
|
3235
4895
|
</summary>
|
|
3236
|
-
<div className="mx-auto mt-2 w-full max-w-lg space-y-
|
|
3237
|
-
|
|
3238
|
-
<
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
4896
|
+
<div className="mx-auto mt-2 w-full max-w-lg space-y-1.5 border-t border-og-border/50 pt-2">
|
|
4897
|
+
<p className="text-og-xs text-og-fg-subtle">
|
|
4898
|
+
Received <time dateTime={item.occurredAt}>{formatClockTime(item.occurredAt)}</time>
|
|
4899
|
+
</p>
|
|
4900
|
+
{groupMachineInputMembers(item.members).map((group) =>
|
|
4901
|
+
group.kind === "agent" ? (
|
|
4902
|
+
<AgentMemberRow
|
|
4903
|
+
key={group.members[0]!.id}
|
|
4904
|
+
members={group.members}
|
|
4905
|
+
onOpenSession={onOpenSession}
|
|
4906
|
+
/>
|
|
4907
|
+
) : (
|
|
4908
|
+
<MachineInputRow
|
|
4909
|
+
key={group.member.id}
|
|
4910
|
+
member={group.member}
|
|
4911
|
+
onOpenSession={onOpenSession}
|
|
4912
|
+
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
4913
|
+
/>
|
|
4914
|
+
),
|
|
4915
|
+
)}
|
|
3244
4916
|
</div>
|
|
3245
4917
|
</details>
|
|
3246
4918
|
{showCollapsedSummary ? (
|
|
@@ -3252,11 +4924,94 @@ function MachineInputBatchRow({
|
|
|
3252
4924
|
);
|
|
3253
4925
|
}
|
|
3254
4926
|
|
|
4927
|
+
type MachineInputMemberGroup =
|
|
4928
|
+
| { kind: "agent"; members: MachineInputBatchItem["members"] }
|
|
4929
|
+
| { kind: "other"; member: MachineInputBatchItem["members"][number] };
|
|
4930
|
+
|
|
4931
|
+
/**
|
|
4932
|
+
* Agent members get named rows; repeated progress notes from one agent fold
|
|
4933
|
+
* into its single row (latest note shown, earlier ones behind the disclosure).
|
|
4934
|
+
*/
|
|
4935
|
+
function groupMachineInputMembers(
|
|
4936
|
+
members: MachineInputBatchItem["members"],
|
|
4937
|
+
): MachineInputMemberGroup[] {
|
|
4938
|
+
const groups: MachineInputMemberGroup[] = [];
|
|
4939
|
+
const progressBySource = new Map<string, MachineInputBatchItem["members"]>();
|
|
4940
|
+
for (const member of members) {
|
|
4941
|
+
if (!isAgentMember(member)) {
|
|
4942
|
+
groups.push({ kind: "other", member });
|
|
4943
|
+
continue;
|
|
4944
|
+
}
|
|
4945
|
+
if (member.kind === "child_progress") {
|
|
4946
|
+
const existing = progressBySource.get(member.sourceId);
|
|
4947
|
+
if (existing) {
|
|
4948
|
+
existing.push(member);
|
|
4949
|
+
continue;
|
|
4950
|
+
}
|
|
4951
|
+
const bucket = [member];
|
|
4952
|
+
progressBySource.set(member.sourceId, bucket);
|
|
4953
|
+
groups.push({ kind: "agent", members: bucket });
|
|
4954
|
+
continue;
|
|
4955
|
+
}
|
|
4956
|
+
groups.push({ kind: "agent", members: [member] });
|
|
4957
|
+
}
|
|
4958
|
+
return groups;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
function agentMemberIcon(member: MachineInputBatchItem["members"][number]): ReactNode {
|
|
4962
|
+
if (member.kind === "child_terminal_result" && member.classification === "failure") {
|
|
4963
|
+
return <XCircleIcon />;
|
|
4964
|
+
}
|
|
4965
|
+
if (member.kind === "child_paused") return <PauseCircleIcon />;
|
|
4966
|
+
return <CornerDownRightIcon />;
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
/** One named row for an update sent by (or about) another agent. */
|
|
4970
|
+
function AgentMemberRow({
|
|
4971
|
+
members,
|
|
4972
|
+
onOpenSession,
|
|
4973
|
+
}: {
|
|
4974
|
+
members: MachineInputBatchItem["members"];
|
|
4975
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
4976
|
+
}) {
|
|
4977
|
+
const identity = useAgentIdentity();
|
|
4978
|
+
const latest = members[members.length - 1]!;
|
|
4979
|
+
const sessionId = agentMemberSessionId(latest);
|
|
4980
|
+
const title = agentMemberTitle(latest, identity.titleFor(sessionId));
|
|
4981
|
+
const texts = members.map((member) => agentMemberText(member));
|
|
4982
|
+
const latestText = texts[texts.length - 1]!;
|
|
4983
|
+
const bodies = texts
|
|
4984
|
+
.map((text, index) => ({ id: members[index]!.id, body: text.body }))
|
|
4985
|
+
.filter((entry) => entry.body.length > 0);
|
|
4986
|
+
return (
|
|
4987
|
+
<AgentRow
|
|
4988
|
+
icon={agentMemberIcon(latest)}
|
|
4989
|
+
title={title}
|
|
4990
|
+
preview={latestText.preview ? truncate(latestText.preview, 320) : null}
|
|
4991
|
+
previewLines={2}
|
|
4992
|
+
meta={members.length > 1 ? `+${members.length - 1} earlier` : null}
|
|
4993
|
+
sessionId={sessionId}
|
|
4994
|
+
onOpenSession={onOpenSession ?? identity.onOpenSession}
|
|
4995
|
+
failed={latest.kind === "child_terminal_result" && latest.classification === "failure"}
|
|
4996
|
+
>
|
|
4997
|
+
{bodies.length > 0 ? (
|
|
4998
|
+
<>
|
|
4999
|
+
{bodies.map((entry) => (
|
|
5000
|
+
<AgentRowText key={entry.id}>{entry.body}</AgentRowText>
|
|
5001
|
+
))}
|
|
5002
|
+
</>
|
|
5003
|
+
) : undefined}
|
|
5004
|
+
</AgentRow>
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
|
|
3255
5008
|
function MachineInputRow({
|
|
3256
5009
|
member,
|
|
5010
|
+
onOpenSession,
|
|
3257
5011
|
loadVideoArtifactPlayback,
|
|
3258
5012
|
}: {
|
|
3259
5013
|
member: MachineInputBatchItem["members"][number];
|
|
5014
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3260
5015
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
3261
5016
|
}) {
|
|
3262
5017
|
if (member.kind === "media_generation_result" && member.result) {
|
|
@@ -3283,6 +5038,11 @@ function MachineInputRow({
|
|
|
3283
5038
|
{truncate(summary, 320)}
|
|
3284
5039
|
</p>
|
|
3285
5040
|
) : null}
|
|
5041
|
+
<ChildSessionLink
|
|
5042
|
+
kind={member.kind}
|
|
5043
|
+
sourceId={member.sourceId}
|
|
5044
|
+
onOpenSession={onOpenSession}
|
|
5045
|
+
/>
|
|
3286
5046
|
</div>
|
|
3287
5047
|
</div>
|
|
3288
5048
|
);
|
|
@@ -3356,12 +5116,36 @@ function formatVideoDuration(seconds: number): string {
|
|
|
3356
5116
|
|
|
3357
5117
|
function NoticeRow({ item }: { item: NoticeItem }) {
|
|
3358
5118
|
const enter = useEntranceAnimation();
|
|
5119
|
+
if (item.allowance) return <AllowanceExhaustedRow refusal={item.allowance} />;
|
|
5120
|
+
if (item.sessionUsageLimit) return <SessionUsageLimitRow stop={item.sessionUsageLimit} />;
|
|
5121
|
+
if (item.recordedOutcome) {
|
|
5122
|
+
return (
|
|
5123
|
+
<details
|
|
5124
|
+
className="group text-og-sm text-og-fg-muted"
|
|
5125
|
+
role="note"
|
|
5126
|
+
data-og-recorded-outcome="wait"
|
|
5127
|
+
>
|
|
5128
|
+
<summary className="flex cursor-pointer list-none items-center gap-2 py-1 [&::-webkit-details-marker]:hidden">
|
|
5129
|
+
<ChevronRightIcon
|
|
5130
|
+
aria-hidden
|
|
5131
|
+
className="size-3.5 transition-transform group-open:rotate-90"
|
|
5132
|
+
/>
|
|
5133
|
+
<span title={new Date(item.occurredAt).toLocaleString()}>
|
|
5134
|
+
{recordedWaitSummary(item)}
|
|
5135
|
+
</span>
|
|
5136
|
+
</summary>
|
|
5137
|
+
<p className="mt-1 whitespace-pre-wrap break-words pl-5 text-og-fg-muted">{item.text}</p>
|
|
5138
|
+
</details>
|
|
5139
|
+
);
|
|
5140
|
+
}
|
|
5141
|
+
const presentedTone = noticeTone(item);
|
|
5142
|
+
const resolvedApproval = noticeIsResolvedApproval(item);
|
|
3359
5143
|
const tone =
|
|
3360
|
-
|
|
5144
|
+
presentedTone === "failed"
|
|
3361
5145
|
? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
|
|
3362
|
-
:
|
|
5146
|
+
: presentedTone === "waiting"
|
|
3363
5147
|
? WAITING_PILL_CLASS
|
|
3364
|
-
:
|
|
5148
|
+
: NEUTRAL_PILL;
|
|
3365
5149
|
return (
|
|
3366
5150
|
<div
|
|
3367
5151
|
className={cn(
|
|
@@ -3371,16 +5155,22 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
3371
5155
|
)}
|
|
3372
5156
|
role="status"
|
|
3373
5157
|
>
|
|
3374
|
-
|
|
3375
|
-
className=
|
|
3376
|
-
|
|
5158
|
+
{resolvedApproval ? (
|
|
5159
|
+
<CheckIcon className="mt-0.5 size-4 shrink-0 opacity-70" aria-hidden="true" />
|
|
5160
|
+
) : (
|
|
5161
|
+
<TriangleAlertIcon
|
|
5162
|
+
className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
|
|
5163
|
+
/>
|
|
5164
|
+
)}
|
|
3377
5165
|
<div className="min-w-0 flex-1">
|
|
3378
|
-
<span className="whitespace-pre-wrap break-words">{item
|
|
5166
|
+
<span className="whitespace-pre-wrap break-words">{noticeDisplayText(item)}</span>
|
|
3379
5167
|
{item.details ? (
|
|
3380
5168
|
<details className="mt-2 text-og-control">
|
|
3381
5169
|
<summary className="cursor-pointer font-medium">{item.details.label}</summary>
|
|
3382
|
-
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-og-sm bg-
|
|
3383
|
-
{
|
|
5170
|
+
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-og-sm bg-og-fg/5 p-2 font-mono">
|
|
5171
|
+
{typeof item.details.value === "string"
|
|
5172
|
+
? item.details.value
|
|
5173
|
+
: JSON.stringify(item.details.value, null, 2)}
|
|
3384
5174
|
</pre>
|
|
3385
5175
|
</details>
|
|
3386
5176
|
) : null}
|
|
@@ -3399,6 +5189,41 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
3399
5189
|
);
|
|
3400
5190
|
}
|
|
3401
5191
|
|
|
5192
|
+
/**
|
|
5193
|
+
* "Waited for 2 agents · 3m 5s" once later input ended the wait, otherwise
|
|
5194
|
+
* "Waiting for 2 agents · since 10:32" (with the date on a later day). The
|
|
5195
|
+
* reason stays behind the disclosure.
|
|
5196
|
+
*/
|
|
5197
|
+
function recordedWaitSummary(item: NoticeItem): ReactNode {
|
|
5198
|
+
const agents = item.waitingAgents ?? 0;
|
|
5199
|
+
const target = agents > 0 ? ` for ${agents} ${agents === 1 ? "agent" : "agents"}` : "";
|
|
5200
|
+
const waitedMs = item.waitEndedAt
|
|
5201
|
+
? durationBetween(item.occurredAt, item.waitEndedAt)
|
|
5202
|
+
: undefined;
|
|
5203
|
+
if (waitedMs !== undefined) {
|
|
5204
|
+
return (
|
|
5205
|
+
<>
|
|
5206
|
+
Waited{target} ·{" "}
|
|
5207
|
+
<time dateTime={`PT${Math.floor(waitedMs / 1000)}S`}>{formatElapsed(waitedMs)}</time>
|
|
5208
|
+
</>
|
|
5209
|
+
);
|
|
5210
|
+
}
|
|
5211
|
+
// An open wait is historical once its day has passed; keep its date visible.
|
|
5212
|
+
const started = new Date(item.occurredAt);
|
|
5213
|
+
const sameDay = started.toDateString() === new Date().toDateString();
|
|
5214
|
+
return (
|
|
5215
|
+
<>
|
|
5216
|
+
{item.waitEndedAt ? "Waited" : "Waiting"}
|
|
5217
|
+
{target} · since{" "}
|
|
5218
|
+
<time dateTime={item.occurredAt}>
|
|
5219
|
+
{sameDay
|
|
5220
|
+
? formatClockTime(item.occurredAt)
|
|
5221
|
+
: started.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })}
|
|
5222
|
+
</time>
|
|
5223
|
+
</>
|
|
5224
|
+
);
|
|
5225
|
+
}
|
|
5226
|
+
|
|
3402
5227
|
/**
|
|
3403
5228
|
* The inline connection-recovery card: missing or lapsed access surfaces as a
|
|
3404
5229
|
* calm, tappable affordance instead of a raw provider-domain error. The `reason`
|
|
@@ -3445,6 +5270,7 @@ function AuthNeededRow({
|
|
|
3445
5270
|
? `${provider} tools unavailable`
|
|
3446
5271
|
: `${actionLabel} ${provider}`;
|
|
3447
5272
|
const reasonLine = recommendation?.rationale ?? authReasonLine(item.reason);
|
|
5273
|
+
const hostAuthorizationUrl = item.authoritySource === "host" ? item.authorizationUrl : null;
|
|
3448
5274
|
|
|
3449
5275
|
const start = async () => {
|
|
3450
5276
|
if (!onReconnect || busy) {
|
|
@@ -3487,27 +5313,40 @@ function AuthNeededRow({
|
|
|
3487
5313
|
) : null}
|
|
3488
5314
|
</div>
|
|
3489
5315
|
</div>
|
|
3490
|
-
{
|
|
5316
|
+
{hostAuthorizationUrl ? (
|
|
5317
|
+
<a
|
|
5318
|
+
href={hostAuthorizationUrl}
|
|
5319
|
+
rel="noreferrer"
|
|
5320
|
+
target="_blank"
|
|
5321
|
+
className={cn(
|
|
5322
|
+
PRIMARY_ACTION_CLASS,
|
|
5323
|
+
"transition-colors hover:bg-og-primary-hover pointer-coarse:min-h-9",
|
|
5324
|
+
)}
|
|
5325
|
+
>
|
|
5326
|
+
<RefreshCwIcon className="size-3.5" aria-hidden />
|
|
5327
|
+
{actionLabel}
|
|
5328
|
+
</a>
|
|
5329
|
+
) : item.authoritySource !== "host" && !unavailable && onReconnect ? (
|
|
3491
5330
|
<button
|
|
3492
5331
|
type="button"
|
|
3493
5332
|
onClick={() => void start()}
|
|
3494
5333
|
disabled={busy}
|
|
3495
5334
|
className={cn(
|
|
3496
5335
|
PRIMARY_ACTION_CLASS,
|
|
3497
|
-
"transition-colors hover:bg-og-
|
|
5336
|
+
"transition-colors hover:bg-og-primary-hover disabled:opacity-50 pointer-coarse:min-h-9",
|
|
3498
5337
|
)}
|
|
3499
5338
|
>
|
|
3500
5339
|
<RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
|
|
3501
5340
|
{busy ? "Opening…" : actionLabel}
|
|
3502
5341
|
</button>
|
|
3503
|
-
) : !unavailable && item.authorizationUrl ? (
|
|
5342
|
+
) : item.authoritySource !== "host" && !unavailable && item.authorizationUrl ? (
|
|
3504
5343
|
<a
|
|
3505
5344
|
href={item.authorizationUrl}
|
|
3506
5345
|
rel="noreferrer"
|
|
3507
5346
|
target="_blank"
|
|
3508
5347
|
className={cn(
|
|
3509
5348
|
PRIMARY_ACTION_CLASS,
|
|
3510
|
-
"transition-colors hover:bg-og-
|
|
5349
|
+
"transition-colors hover:bg-og-primary-hover pointer-coarse:min-h-9",
|
|
3511
5350
|
)}
|
|
3512
5351
|
>
|
|
3513
5352
|
<RefreshCwIcon className="size-3.5" aria-hidden />
|