@opengeni/react 3.3.2 → 3.4.0
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 +2 -0
- 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-4EUKJXUO.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-XYRDDSXF.js → chunk-2I6R3GCZ.js} +4285 -3980
- package/dist/chunk-2I6R3GCZ.js.map +1 -0
- package/dist/{chunk-JAQ5K7W6.js → chunk-2SFFYIH3.js} +1178 -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-3RJKUVCA.js +14068 -0
- package/dist/chunk-3RJKUVCA.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-ZPBTI3TE.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-YVCHAQZY.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-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-IOLVN74D.js → chunk-V5ZUJT2T.js} +175 -42
- package/dist/chunk-V5ZUJT2T.js.map +1 -0
- package/dist/chunk-WBJY57A3.js +402 -0
- package/dist/chunk-WBJY57A3.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-3AGBGTRT.js → chunk-X5UVUIMF.js} +1712 -1174
- 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/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 +35 -28
- 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/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 +26 -5
- 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 +24 -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 +2340 -6603
- 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 -1333
- 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/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 +106 -7
- 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 +193 -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 +5 -3
- 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 +2370 -667
- 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 +252 -429
- 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/tool-action-review.tsx +650 -0
- package/src/components/tool-review-history.tsx +88 -0
- 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 +2 -2
- 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 +227 -44
- 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 +79 -9
- package/src/hooks/use-last-started-turn-policy.ts +37 -11
- 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 +531 -335
- 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 +30 -2
- 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 +74 -7
- 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 +205 -10
- 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 -550
- 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/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 +1742 -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-MKKFRRRV.js +0 -14
- package/dist/chunk-2DZVYHVO.js +0 -470
- package/dist/chunk-2DZVYHVO.js.map +0 -1
- package/dist/chunk-3AGBGTRT.js.map +0 -1
- package/dist/chunk-4MDLEHXL.js.map +0 -1
- package/dist/chunk-5YK2ZDIG.js.map +0 -1
- package/dist/chunk-7EAFGICR.js +0 -302
- package/dist/chunk-7EAFGICR.js.map +0 -1
- package/dist/chunk-ELIYWBZR.js.map +0 -1
- package/dist/chunk-H5YGYNXS.js.map +0 -1
- package/dist/chunk-HUIKIDM5.js.map +0 -1
- package/dist/chunk-ICA6R6TW.js.map +0 -1
- package/dist/chunk-IOLVN74D.js.map +0 -1
- package/dist/chunk-JAQ5K7W6.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-NEXLLTP4.js.map +0 -1
- package/dist/chunk-PEI6RANI.js.map +0 -1
- package/dist/chunk-VTOREEXF.js +0 -142
- package/dist/chunk-VTOREEXF.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-XYRDDSXF.js.map +0 -1
- package/dist/chunk-YG4QEQF5.js +0 -9557
- package/dist/chunk-YG4QEQF5.js.map +0 -1
- package/dist/chunk-YVCHAQZY.js.map +0 -1
- package/dist/chunk-ZPBTI3TE.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-MKKFRRRV.js.map → browser-viewer-ZWR6MWJ7.js.map} +0 -0
- /package/dist/{computer-viewer-FWEZISW4.js.map → chat-interactive-block-4EUKJXUO.js.map} +0 -0
|
@@ -1,3 +1,35 @@
|
|
|
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,
|
|
@@ -8,10 +40,13 @@ import { dequal } from "dequal/lite";
|
|
|
8
40
|
import {
|
|
9
41
|
ArrowDownIcon,
|
|
10
42
|
ArrowRightIcon,
|
|
43
|
+
ArrowUpIcon,
|
|
11
44
|
BotIcon,
|
|
12
45
|
CheckCircle2Icon,
|
|
13
46
|
CheckIcon,
|
|
47
|
+
ChevronDownIcon,
|
|
14
48
|
ChevronRightIcon,
|
|
49
|
+
CornerDownRightIcon,
|
|
15
50
|
PauseCircleIcon,
|
|
16
51
|
PauseIcon,
|
|
17
52
|
MessageCircleQuestionIcon,
|
|
@@ -24,16 +59,19 @@ import {
|
|
|
24
59
|
TriangleAlertIcon,
|
|
25
60
|
XCircleIcon,
|
|
26
61
|
} from "lucide-react";
|
|
27
|
-
import type { ComponentType } from "react";
|
|
28
|
-
import { AnimatePresence, motion } from "motion/react";
|
|
29
|
-
import { Collapsible } from "radix-ui";
|
|
62
|
+
import type { ComponentType, CSSProperties } from "react";
|
|
63
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
30
64
|
import {
|
|
31
65
|
Component,
|
|
66
|
+
Fragment,
|
|
32
67
|
Suspense,
|
|
68
|
+
createContext,
|
|
33
69
|
lazy,
|
|
34
70
|
memo,
|
|
35
71
|
useCallback,
|
|
72
|
+
useContext,
|
|
36
73
|
useEffect,
|
|
74
|
+
useId,
|
|
37
75
|
useLayoutEffect,
|
|
38
76
|
useMemo,
|
|
39
77
|
useRef,
|
|
@@ -41,6 +79,11 @@ import {
|
|
|
41
79
|
type ReactNode,
|
|
42
80
|
} from "react";
|
|
43
81
|
import { cn } from "../lib/cn";
|
|
82
|
+
import {
|
|
83
|
+
MessageSenderContext,
|
|
84
|
+
MessageSenderSlot,
|
|
85
|
+
type RenderMessageSender,
|
|
86
|
+
} from "../timeline/message-sender";
|
|
44
87
|
import { formatClockTime, formatRelativeTime, truncate } from "../lib/format";
|
|
45
88
|
import { prefersReducedMotion } from "../lib/motion";
|
|
46
89
|
import {
|
|
@@ -48,11 +91,27 @@ import {
|
|
|
48
91
|
type OlderHistoryLoadReceipt,
|
|
49
92
|
type OlderHistoryLoader,
|
|
50
93
|
} from "../older-history";
|
|
51
|
-
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";
|
|
52
110
|
import {
|
|
53
111
|
UserMessageBody,
|
|
54
112
|
UserMessageDisclosureProvider,
|
|
55
113
|
type UserMessageDisclosureContextValue,
|
|
114
|
+
type UserMessageDisclosureLabels,
|
|
56
115
|
} from "./user-message-body";
|
|
57
116
|
import {
|
|
58
117
|
createTipFollowState,
|
|
@@ -89,6 +148,7 @@ import {
|
|
|
89
148
|
type VideoArtifactPlaybackLoader,
|
|
90
149
|
type ToolRegistry,
|
|
91
150
|
type TurnSummaryOptions,
|
|
151
|
+
type TurnSummaryStatus,
|
|
92
152
|
type UserMessageItem,
|
|
93
153
|
type FoldRestingState,
|
|
94
154
|
type WorkerCompletionItem,
|
|
@@ -99,42 +159,104 @@ import {
|
|
|
99
159
|
useTurnSettleOpen,
|
|
100
160
|
} from "../timeline";
|
|
101
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";
|
|
102
165
|
import { GeneratedVideoPlayer } from "./generated-video-player";
|
|
103
166
|
import {
|
|
104
167
|
MACHINE_INPUT_META,
|
|
168
|
+
agentMemberSessionId,
|
|
169
|
+
agentMemberText,
|
|
170
|
+
agentMemberTitle,
|
|
105
171
|
cleanMachineInputSummary,
|
|
172
|
+
isAgentMember,
|
|
106
173
|
machineInputBatchLabel,
|
|
107
174
|
machineInputSummaryIsUseful,
|
|
108
175
|
readableMachineInputSource,
|
|
109
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";
|
|
110
184
|
import { SESSION_STATUS_META, StatusDot } from "./session-status";
|
|
185
|
+
import {
|
|
186
|
+
noticeDisplayText,
|
|
187
|
+
noticeIsResolvedApproval,
|
|
188
|
+
noticeTone,
|
|
189
|
+
} from "../timeline/notice-presentation";
|
|
111
190
|
import { TimelineComputeLabelProvider } from "../timeline/compute-label";
|
|
191
|
+
import { ProviderLogoProvider } from "../timeline/provider-logo";
|
|
112
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";
|
|
113
200
|
import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
|
|
114
|
-
import {
|
|
201
|
+
import { TimelineAnnotationCards } from "./timeline-annotations";
|
|
115
202
|
import { TooltipProvider } from "./tooltip";
|
|
116
203
|
|
|
117
204
|
const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-selection"));
|
|
205
|
+
const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
|
|
118
206
|
|
|
119
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;
|
|
120
218
|
/** Raw session events (projected internally) … */
|
|
121
219
|
events?: SessionEvent[] | undefined;
|
|
122
220
|
/** … or pre-projected items (e.g. from `useSessionEvents().timeline`). */
|
|
123
221
|
items?: TimelineItem[] | undefined;
|
|
124
222
|
/** Current session status (reserved; tip "Working…" chrome removed for now). */
|
|
125
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;
|
|
126
232
|
/** Plug a markdown renderer for message bodies (e.g. streamdown). */
|
|
127
233
|
renderMessageText?:
|
|
128
|
-
| ((
|
|
234
|
+
| ((
|
|
235
|
+
text: string,
|
|
236
|
+
item: AgentMessageItem | UserMessageItem,
|
|
237
|
+
context: { searchTarget: TimelineSearchTarget | null },
|
|
238
|
+
) => ReactNode)
|
|
129
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"];
|
|
130
245
|
/** Drill into a spawned worker session. */
|
|
131
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;
|
|
132
254
|
/**
|
|
133
255
|
* Deep-link a memory row (a `memory.saved` / `memory.corrected` step) to its
|
|
134
256
|
* record in the host's memory pane. Opt-in, exactly like `onReconnect`: the
|
|
135
257
|
* library draws no "View in memory" affordance without a handler — the memory
|
|
136
258
|
* row is then non-interactive rich content. This is the switch that makes the
|
|
137
|
-
* deep-link a first-party
|
|
259
|
+
* deep-link a first-party Opengeni capability without other SDK consumers
|
|
138
260
|
* opting into it. The app supplies it (it owns routing to the memory pane).
|
|
139
261
|
*/
|
|
140
262
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
@@ -146,6 +268,17 @@ export type MessageTimelineProps = {
|
|
|
146
268
|
* a Reconnect button without a handler to run it.
|
|
147
269
|
*/
|
|
148
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;
|
|
149
282
|
/**
|
|
150
283
|
* Decide which durable authentication notices this timeline presents.
|
|
151
284
|
* Defaults to showing every notice. Embedded hosts can suppress notices for
|
|
@@ -166,6 +299,16 @@ export type MessageTimelineProps = {
|
|
|
166
299
|
* `createDefaultToolRegistry({ entries })` to add custom tool renderers.
|
|
167
300
|
*/
|
|
168
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;
|
|
169
312
|
/** Resolve opaque retained screenshot receipts through the authenticated host SDK. */
|
|
170
313
|
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
171
314
|
/** Resolve permanent workspace image/file receipts through the authenticated host SDK. */
|
|
@@ -178,11 +321,22 @@ export type MessageTimelineProps = {
|
|
|
178
321
|
*/
|
|
179
322
|
computeLabel?: string | null | undefined;
|
|
180
323
|
/** Customize collapsed turn facets for this timeline instance. */
|
|
324
|
+
genieLoading?: GenieLoadingOptions | undefined;
|
|
181
325
|
turnSummary?: TurnSummaryOptions | undefined;
|
|
182
326
|
/** Follow new events when pinned to the bottom. Defaults to true. */
|
|
183
327
|
autoFollow?: boolean | undefined;
|
|
184
328
|
/** Capture a same-row text selection into the host's canonical composer draft. */
|
|
185
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;
|
|
186
340
|
/** Older durable history exists above the current window (see useSessionEvents). */
|
|
187
341
|
hasOlder?: boolean | undefined;
|
|
188
342
|
/** An older window is being fetched; shows the quiet top shimmer. */
|
|
@@ -201,13 +355,16 @@ export type MessageTimelineProps = {
|
|
|
201
355
|
hasNewer?: boolean | undefined;
|
|
202
356
|
/** A newer history page is being fetched. */
|
|
203
357
|
loadingNewer?: boolean | undefined;
|
|
204
|
-
/** Page forward through history
|
|
205
|
-
onLoadNewer?: (() =>
|
|
358
|
+
/** Page forward through history. Return the request promise to enable inline error/retry. */
|
|
359
|
+
onLoadNewer?: (() => unknown) | undefined;
|
|
206
360
|
/**
|
|
207
361
|
* Reload the live tip window. When omitted, Jump to latest only re-pins and
|
|
208
362
|
* scrolls the in-memory window.
|
|
209
363
|
*/
|
|
210
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;
|
|
211
368
|
/** Host-owned content appended after timeline groups, such as startup progress. */
|
|
212
369
|
trailingState?: ReactNode | undefined;
|
|
213
370
|
emptyState?: ReactNode | undefined;
|
|
@@ -246,6 +403,24 @@ const PIN_THRESHOLD_PX = 48;
|
|
|
246
403
|
* line-sized streaming movement.
|
|
247
404
|
*/
|
|
248
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;
|
|
249
424
|
/**
|
|
250
425
|
* Prefetch older history when the top sentinel is this far from the viewport.
|
|
251
426
|
* After a page loads we stay cool until the reader leaves this band (scrolls
|
|
@@ -254,13 +429,21 @@ const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
|
|
|
254
429
|
const OLDER_PREFETCH_MARGIN_PX = 400;
|
|
255
430
|
const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`;
|
|
256
431
|
const PRIMARY_ACTION_CLASS =
|
|
257
|
-
"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";
|
|
258
433
|
const MESSAGE_BUBBLE_CLASS =
|
|
259
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";
|
|
260
435
|
const WAITING_PILL_CLASS =
|
|
261
436
|
"border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting";
|
|
262
437
|
const LOADING_CHIP_CLASS =
|
|
263
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);
|
|
264
447
|
|
|
265
448
|
// State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled,
|
|
266
449
|
// 3 explicit no-progress waiting to become an underfill retry.
|
|
@@ -278,13 +461,18 @@ function invokeOlderLoad(
|
|
|
278
461
|
load: OlderHistoryLoader,
|
|
279
462
|
noProgress: () => void,
|
|
280
463
|
attempt: OlderLoadAttempt,
|
|
464
|
+
preserveTail = false,
|
|
281
465
|
): 1 | undefined {
|
|
282
466
|
try {
|
|
283
467
|
// Receipt creation is captured synchronously through legacy wrappers such
|
|
284
468
|
// as `() => void loadOlder()`, even when the wrapper discards the return.
|
|
285
|
-
const result = invokeOlderHistoryLoaderWithReceiptCapture(
|
|
286
|
-
|
|
287
|
-
|
|
469
|
+
const result = invokeOlderHistoryLoaderWithReceiptCapture(
|
|
470
|
+
load,
|
|
471
|
+
(receipt) => {
|
|
472
|
+
attempt[2] = receipt;
|
|
473
|
+
},
|
|
474
|
+
preserveTail,
|
|
475
|
+
) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
|
|
288
476
|
const receipt =
|
|
289
477
|
attempt[2] ??
|
|
290
478
|
(typeof (result as { committed?: unknown } | undefined)?.committed === "boolean"
|
|
@@ -364,6 +552,49 @@ function isNearBottom(node: HTMLElement): boolean {
|
|
|
364
552
|
return gap < Math.min(PIN_THRESHOLD_PX, maxScroll);
|
|
365
553
|
}
|
|
366
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
|
+
|
|
367
598
|
/** Escape a value for use inside a CSS attribute selector. */
|
|
368
599
|
function cssEscapeAttribute(value: string): string {
|
|
369
600
|
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
@@ -379,13 +610,24 @@ function cssEscapeAttribute(value: string): string {
|
|
|
379
610
|
* with a "jump to latest" affordance when the reader scrolls back.
|
|
380
611
|
*/
|
|
381
612
|
export function MessageTimeline({
|
|
613
|
+
resolveLink,
|
|
614
|
+
renderInteractiveBlock,
|
|
615
|
+
userMessageDisclosureLabels,
|
|
616
|
+
searchTarget,
|
|
617
|
+
focusSequence,
|
|
382
618
|
events,
|
|
383
619
|
items,
|
|
384
620
|
status: _status,
|
|
621
|
+
renderMessageActions,
|
|
622
|
+
renderMessageSender,
|
|
385
623
|
renderMessageText,
|
|
386
624
|
onOpenSession,
|
|
625
|
+
resolveSessionTitle,
|
|
387
626
|
onMemoryClick,
|
|
388
627
|
onReconnect,
|
|
628
|
+
renderAuthNeeded,
|
|
629
|
+
renderAllowanceExhausted,
|
|
630
|
+
allowanceExhaustedLabels,
|
|
389
631
|
shouldRenderAuthNeeded,
|
|
390
632
|
resolveProviderLogo,
|
|
391
633
|
toolRegistry = defaultToolRegistry,
|
|
@@ -393,9 +635,13 @@ export function MessageTimeline({
|
|
|
393
635
|
loadRetainedArtifact,
|
|
394
636
|
loadVideoArtifactPlayback,
|
|
395
637
|
computeLabel = null,
|
|
638
|
+
genieLoading,
|
|
396
639
|
turnSummary,
|
|
397
640
|
autoFollow = true,
|
|
398
641
|
onAnnotate,
|
|
642
|
+
questionNavMinViewportHeight = 0,
|
|
643
|
+
draftAnnotations,
|
|
644
|
+
onDraftAnnotationSelect,
|
|
399
645
|
hasOlder = false,
|
|
400
646
|
loadingOlder = false,
|
|
401
647
|
onLoadOlder,
|
|
@@ -409,6 +655,7 @@ export function MessageTimeline({
|
|
|
409
655
|
emptyState,
|
|
410
656
|
className,
|
|
411
657
|
}: MessageTimelineProps) {
|
|
658
|
+
const formatError = useErrorMessage();
|
|
412
659
|
const resolvedItems = useMemo(() => {
|
|
413
660
|
const projectedItems = items ?? buildTimeline(events ?? []);
|
|
414
661
|
if (!shouldRenderAuthNeeded) {
|
|
@@ -418,9 +665,134 @@ export function MessageTimeline({
|
|
|
418
665
|
(item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
|
|
419
666
|
);
|
|
420
667
|
}, [items, events, shouldRenderAuthNeeded]);
|
|
421
|
-
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;
|
|
422
715
|
const olderBoundaryKey = sourceItems?.[0]?.id;
|
|
423
|
-
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]);
|
|
424
796
|
const annotationSources = useMemo(() => {
|
|
425
797
|
const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
|
|
426
798
|
for (const item of resolvedItems) {
|
|
@@ -440,6 +812,11 @@ export function MessageTimeline({
|
|
|
440
812
|
const bottomSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
441
813
|
const previousBulkFirstKeyRef = useRef<string | null | undefined>(undefined);
|
|
442
814
|
const [pinned, setPinned] = useState(true);
|
|
815
|
+
const { groups: allGroups, release: releaseProgress } = useReadingProgress(
|
|
816
|
+
projectedGroups,
|
|
817
|
+
autoFollow && pinned && !hasNewer,
|
|
818
|
+
scrollRef.current,
|
|
819
|
+
);
|
|
443
820
|
const [canSkipTipCatchup, setCanSkipTipCatchup] = useState(false);
|
|
444
821
|
const canSkipTipCatchupRef = useRef(false);
|
|
445
822
|
const [bulkActive, setBulkActive] = useState(true);
|
|
@@ -461,6 +838,17 @@ export function MessageTimeline({
|
|
|
461
838
|
);
|
|
462
839
|
const underfillRetryReadyRef = useRef(false);
|
|
463
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);
|
|
464
852
|
const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
|
|
465
853
|
// Content stays invisible until the tip is hard-parked across a short
|
|
466
854
|
// post-commit settle (two rAFs). That absorbs sync late layout while hidden
|
|
@@ -507,7 +895,7 @@ export function MessageTimeline({
|
|
|
507
895
|
* jumps (Vimium) settle via scrollend while the camera is idle.
|
|
508
896
|
*/
|
|
509
897
|
const readerIntentArmRef = useRef(false);
|
|
510
|
-
/** Gesture-start geometry
|
|
898
|
+
/** Gesture-start geometry survives commits before the native scroll event. */
|
|
511
899
|
const readerIntentStartRef = useRef<{ scrollTop: number; maxScroll: number } | null>(null);
|
|
512
900
|
/**
|
|
513
901
|
* Count of camera/snap scrollTop writes whose scroll echoes are not yet
|
|
@@ -533,6 +921,191 @@ export function MessageTimeline({
|
|
|
533
921
|
// deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
|
|
534
922
|
// that already settled closed — or that the reader closed — never reopens.
|
|
535
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
|
+
);
|
|
536
1109
|
const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
|
|
537
1110
|
const seenActivityIdsRef = useRef<Set<string>>(new Set());
|
|
538
1111
|
const firstItemGroupKeyRef = useRef<string | null>(null);
|
|
@@ -557,6 +1130,34 @@ export function MessageTimeline({
|
|
|
557
1130
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
558
1131
|
const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
559
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
|
+
);
|
|
560
1161
|
|
|
561
1162
|
const applyCanSkipTipCatchup = useCallback((value: boolean) => {
|
|
562
1163
|
if (canSkipTipCatchupRef.current !== value) {
|
|
@@ -611,6 +1212,9 @@ export function MessageTimeline({
|
|
|
611
1212
|
if (before === next) {
|
|
612
1213
|
return;
|
|
613
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;
|
|
614
1218
|
programmaticScrollRef.current += 1;
|
|
615
1219
|
node.scrollTop = next;
|
|
616
1220
|
if (node.scrollTop === before) {
|
|
@@ -620,6 +1224,7 @@ export function MessageTimeline({
|
|
|
620
1224
|
programmaticScrollRef.current -= 1;
|
|
621
1225
|
}
|
|
622
1226
|
}, []);
|
|
1227
|
+
writeScrollTopRef.current = writeScrollTop;
|
|
623
1228
|
|
|
624
1229
|
const cancelLeaveFallback = useCallback(() => {
|
|
625
1230
|
if (leaveFallbackRafRef.current != null) {
|
|
@@ -648,29 +1253,43 @@ export function MessageTimeline({
|
|
|
648
1253
|
}, []);
|
|
649
1254
|
|
|
650
1255
|
/** Reader left the tip — wheel, keyboard, pointer-armed scroll-up, or scrollend. */
|
|
651
|
-
const releasePinFromReader = useCallback(
|
|
652
|
-
(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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;
|
|
670
1288
|
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
1289
|
+
};
|
|
1290
|
+
node.ownerDocument.addEventListener("selectionchange", onSelection);
|
|
1291
|
+
return () => node.ownerDocument.removeEventListener("selectionchange", onSelection);
|
|
1292
|
+
}, [readableTurns, releasePinFromReader, stopSettlement]);
|
|
674
1293
|
|
|
675
1294
|
/**
|
|
676
1295
|
* Settled away from the tip while the camera is idle — Vimium / unfocused
|
|
@@ -691,7 +1310,7 @@ export function MessageTimeline({
|
|
|
691
1310
|
clearPendingReaderLeave();
|
|
692
1311
|
return;
|
|
693
1312
|
}
|
|
694
|
-
releasePinFromReader(
|
|
1313
|
+
releasePinFromReader();
|
|
695
1314
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
696
1315
|
},
|
|
697
1316
|
[autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
|
|
@@ -712,6 +1331,22 @@ export function MessageTimeline({
|
|
|
712
1331
|
});
|
|
713
1332
|
}, [cancelLeaveFallback, releasePinAfterScrollSettled]);
|
|
714
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
|
+
|
|
715
1350
|
const onWheel = (event: {
|
|
716
1351
|
deltaY: number;
|
|
717
1352
|
deltaX: number;
|
|
@@ -728,13 +1363,17 @@ export function MessageTimeline({
|
|
|
728
1363
|
if (wheelConsumedByNestedScrollable(event)) {
|
|
729
1364
|
return;
|
|
730
1365
|
}
|
|
1366
|
+
stopSettlement();
|
|
731
1367
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1368
|
+
programmaticScrollRef.current = 0;
|
|
732
1369
|
if (event.deltaY >= 0) {
|
|
1370
|
+
const node = scrollRef.current;
|
|
1371
|
+
readerIntentStartRef.current = node
|
|
1372
|
+
? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
|
|
1373
|
+
: null;
|
|
733
1374
|
return;
|
|
734
1375
|
}
|
|
735
|
-
|
|
736
|
-
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
737
|
-
releasePinFromReader(node);
|
|
1376
|
+
requestEarlierFromReader();
|
|
738
1377
|
};
|
|
739
1378
|
|
|
740
1379
|
/** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
|
|
@@ -748,6 +1387,7 @@ export function MessageTimeline({
|
|
|
748
1387
|
if (event.button && event.pointerType === "mouse") {
|
|
749
1388
|
return;
|
|
750
1389
|
}
|
|
1390
|
+
stopSettlement();
|
|
751
1391
|
// Clicks on chips/buttons/links must not arm — their settle collapse
|
|
752
1392
|
// also drops scrollTop and would false-unpin. Drag on prose/scroller may.
|
|
753
1393
|
if (
|
|
@@ -757,6 +1397,7 @@ export function MessageTimeline({
|
|
|
757
1397
|
return;
|
|
758
1398
|
}
|
|
759
1399
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1400
|
+
programmaticScrollRef.current = 0;
|
|
760
1401
|
const node =
|
|
761
1402
|
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
762
1403
|
readerIntentArmRef.current = true;
|
|
@@ -774,14 +1415,20 @@ export function MessageTimeline({
|
|
|
774
1415
|
event.key === "Home" ||
|
|
775
1416
|
event.key === "End"
|
|
776
1417
|
) {
|
|
1418
|
+
stopSettlement();
|
|
777
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;
|
|
778
1427
|
}
|
|
779
1428
|
if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
|
|
780
1429
|
return;
|
|
781
1430
|
}
|
|
782
|
-
|
|
783
|
-
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
784
|
-
releasePinFromReader(node);
|
|
1431
|
+
requestEarlierFromReader();
|
|
785
1432
|
};
|
|
786
1433
|
|
|
787
1434
|
const snapToBottom = useCallback(
|
|
@@ -864,22 +1511,33 @@ export function MessageTimeline({
|
|
|
864
1511
|
() => ({
|
|
865
1512
|
expandedByMessageId: userMessageDisclosureMemoryRef.current,
|
|
866
1513
|
beginChange: beginUserMessageDisclosureChange,
|
|
1514
|
+
labels: {
|
|
1515
|
+
showMore: userMessageDisclosureLabels?.showMore,
|
|
1516
|
+
showLess: userMessageDisclosureLabels?.showLess,
|
|
1517
|
+
},
|
|
867
1518
|
}),
|
|
868
|
-
[
|
|
1519
|
+
[
|
|
1520
|
+
beginUserMessageDisclosureChange,
|
|
1521
|
+
userMessageDisclosureLabels?.showMore,
|
|
1522
|
+
userMessageDisclosureLabels?.showLess,
|
|
1523
|
+
],
|
|
869
1524
|
);
|
|
870
1525
|
const timelineGroupEntryContext = useMemo<TimelineGroupEntryContext>(
|
|
871
1526
|
() => ({
|
|
872
1527
|
userMessageDisclosureContext,
|
|
873
1528
|
behavior: {
|
|
874
|
-
|
|
1529
|
+
renderMessageActions,
|
|
1530
|
+
renderMessageText: renderSearchableMessageText,
|
|
875
1531
|
onOpenSession,
|
|
876
1532
|
onMemoryClick,
|
|
877
1533
|
onReconnect,
|
|
1534
|
+
renderAuthNeeded,
|
|
878
1535
|
resolveProviderLogo,
|
|
879
1536
|
toolRegistry,
|
|
880
1537
|
loadRetainedScreenshot,
|
|
881
1538
|
loadRetainedArtifact,
|
|
882
1539
|
loadVideoArtifactPlayback,
|
|
1540
|
+
genieLoading,
|
|
883
1541
|
turnSummary,
|
|
884
1542
|
},
|
|
885
1543
|
}),
|
|
@@ -890,14 +1548,30 @@ export function MessageTimeline({
|
|
|
890
1548
|
onMemoryClick,
|
|
891
1549
|
onOpenSession,
|
|
892
1550
|
onReconnect,
|
|
893
|
-
|
|
1551
|
+
renderAuthNeeded,
|
|
1552
|
+
renderMessageActions,
|
|
1553
|
+
renderSearchableMessageText,
|
|
894
1554
|
resolveProviderLogo,
|
|
895
1555
|
toolRegistry,
|
|
1556
|
+
genieLoading,
|
|
896
1557
|
turnSummary,
|
|
897
1558
|
userMessageDisclosureContext,
|
|
898
1559
|
],
|
|
899
1560
|
);
|
|
900
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,
|
|
1573
|
+
);
|
|
1574
|
+
|
|
901
1575
|
const requestOlderIfUnderfilled = useCallback(
|
|
902
1576
|
(node: HTMLElement, retry?: OlderLoadAttempt) => {
|
|
903
1577
|
let currentAttempt = olderLoadAttemptRef.current;
|
|
@@ -947,10 +1621,78 @@ export function MessageTimeline({
|
|
|
947
1621
|
// exact `false` is the first-party request-not-accepted receipt.
|
|
948
1622
|
// All other fulfillment retains this exact owner until its prepend
|
|
949
1623
|
// boundary commits; promise settlement alone cannot prove progress.
|
|
950
|
-
invokeOlderLoad(onLoadOlder, noProgress, attempt);
|
|
1624
|
+
invokeOlderLoad(onLoadOlder, noProgress, attempt, !retry);
|
|
951
1625
|
},
|
|
952
1626
|
[hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
|
|
953
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
|
+
|
|
954
1696
|
const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
|
|
955
1697
|
requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
|
|
956
1698
|
);
|
|
@@ -1103,15 +1845,28 @@ export function MessageTimeline({
|
|
|
1103
1845
|
previousMaxScroll <= 1 ||
|
|
1104
1846
|
previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
|
|
1105
1847
|
const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
|
|
1106
|
-
const
|
|
1107
|
-
|
|
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;
|
|
1108
1858
|
const attempt = olderLoadAttemptRef.current;
|
|
1109
1859
|
const committedZeroOverlapOlderReplacement = !!(
|
|
1110
1860
|
attempt?.[2]?.committed &&
|
|
1111
|
-
|
|
1112
|
-
!
|
|
1861
|
+
sourceChanged &&
|
|
1862
|
+
!retainedSource
|
|
1113
1863
|
);
|
|
1114
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
|
+
}
|
|
1115
1870
|
// Keep the reader on the same retained rows. Prefer the exact first-item
|
|
1116
1871
|
// content coordinate (needed when a prepend merges inside one group),
|
|
1117
1872
|
// then the retained group offset, then scrollHeight as a final fallback.
|
|
@@ -1183,17 +1938,23 @@ export function MessageTimeline({
|
|
|
1183
1938
|
applyPinned(false);
|
|
1184
1939
|
}
|
|
1185
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;
|
|
1186
1947
|
if (
|
|
1187
1948
|
autoFollow &&
|
|
1188
1949
|
pinnedRef.current &&
|
|
1189
1950
|
!hasNewer &&
|
|
1190
1951
|
!pendingReaderLeaveRef.current &&
|
|
1191
|
-
(wasAtLiveTailBeforeCommit ||
|
|
1952
|
+
(wasAtLiveTailBeforeCommit || underfillOwned)
|
|
1192
1953
|
) {
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
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.
|
|
1197
1958
|
clearPendingReaderLeave();
|
|
1198
1959
|
snapToBottom(node);
|
|
1199
1960
|
} else {
|
|
@@ -1206,6 +1967,22 @@ export function MessageTimeline({
|
|
|
1206
1967
|
applyPinned(false);
|
|
1207
1968
|
}
|
|
1208
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
|
+
}
|
|
1209
1986
|
} else if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1210
1987
|
// Load/remount (still hidden): hard-park. Live tip after reveal: ease.
|
|
1211
1988
|
// Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
|
|
@@ -1227,6 +2004,7 @@ export function MessageTimeline({
|
|
|
1227
2004
|
if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
|
|
1228
2005
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1229
2006
|
}
|
|
2007
|
+
scheduleQuestionNav();
|
|
1230
2008
|
previousFirstItemIdRef.current = firstItemId;
|
|
1231
2009
|
previousScrollHeightRef.current = node.scrollHeight;
|
|
1232
2010
|
syncScrollBaseline(node);
|
|
@@ -1257,6 +2035,8 @@ export function MessageTimeline({
|
|
|
1257
2035
|
: null;
|
|
1258
2036
|
}
|
|
1259
2037
|
|
|
2038
|
+
if (settlement) settlementAnimationRef.current = animateTimelineSettlement(settlement);
|
|
2039
|
+
|
|
1260
2040
|
// Promise settlement is not itself permission to retry. A receipt-marked
|
|
1261
2041
|
// accepted page retires its exact owner on this commit even when projection
|
|
1262
2042
|
// is empty or merges into the same first item. Without that mark, retain
|
|
@@ -1283,6 +2063,19 @@ export function MessageTimeline({
|
|
|
1283
2063
|
// Boundary progress retires the request owner. A late settlement from
|
|
1284
2064
|
// that completed prefetch cannot mutate the new window's cooldown owner.
|
|
1285
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
|
+
}
|
|
1286
2079
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1287
2080
|
requestOlderIfUnderfilled(node);
|
|
1288
2081
|
});
|
|
@@ -1453,6 +2246,7 @@ export function MessageTimeline({
|
|
|
1453
2246
|
loadingOlder,
|
|
1454
2247
|
olderBoundaryKey,
|
|
1455
2248
|
olderPrefetchArmed,
|
|
2249
|
+
olderDemand,
|
|
1456
2250
|
onLoadOlder,
|
|
1457
2251
|
requestOlderIfUnderfilled,
|
|
1458
2252
|
]);
|
|
@@ -1474,15 +2268,17 @@ export function MessageTimeline({
|
|
|
1474
2268
|
}
|
|
1475
2269
|
const observer = new IntersectionObserver(
|
|
1476
2270
|
(entries) => {
|
|
1477
|
-
|
|
1478
|
-
|
|
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();
|
|
1479
2275
|
}
|
|
1480
2276
|
},
|
|
1481
2277
|
{ root, rootMargin: "0px 0px 1200px 0px" },
|
|
1482
2278
|
);
|
|
1483
2279
|
observer.observe(target);
|
|
1484
2280
|
return () => observer.disconnect();
|
|
1485
|
-
}, [hasNewer, loadingNewer, onLoadNewer, firstGroupKey]);
|
|
2281
|
+
}, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);
|
|
1486
2282
|
|
|
1487
2283
|
// Late layout that React commits cannot see (images decoding, fonts, code
|
|
1488
2284
|
// blocks) grows content without a commit. While pinned, soft-follow the tip;
|
|
@@ -1504,6 +2300,9 @@ export function MessageTimeline({
|
|
|
1504
2300
|
if (!current) {
|
|
1505
2301
|
return;
|
|
1506
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();
|
|
1507
2306
|
requestOlderIfUnderfilled(current);
|
|
1508
2307
|
if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
|
|
1509
2308
|
return;
|
|
@@ -1535,6 +2334,8 @@ export function MessageTimeline({
|
|
|
1535
2334
|
// at what just became the live bottom — paging forward to the tip must not
|
|
1536
2335
|
// strand them unpinned watching new content grow below.
|
|
1537
2336
|
useEffect(() => {
|
|
2337
|
+
// Closing find is paint-only; it must not trigger this history-to-tip rule.
|
|
2338
|
+
if (searchTargetRef.current) return;
|
|
1538
2339
|
if (hasNewer) {
|
|
1539
2340
|
stopFollow();
|
|
1540
2341
|
applyPinned(false);
|
|
@@ -1557,6 +2358,15 @@ export function MessageTimeline({
|
|
|
1557
2358
|
}
|
|
1558
2359
|
}, [hasNewer, autoFollow, applyPinned, snapToBottom, stopFollow]);
|
|
1559
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
|
+
|
|
1560
2370
|
// Pinned: layout/camera recover tip debt; wheel/keys/pointer-arm unpin
|
|
1561
2371
|
// immediately; extension jumps settle via scrollend (or one-rAF fallback).
|
|
1562
2372
|
// Do not tip-follow-yank an in-flight unarmed scroll-away — that ate Vimium.
|
|
@@ -1565,6 +2375,9 @@ export function MessageTimeline({
|
|
|
1565
2375
|
if (!node) {
|
|
1566
2376
|
return;
|
|
1567
2377
|
}
|
|
2378
|
+
// Only an unpinned reader's distance matters for the jump.
|
|
2379
|
+
if (!pinnedRef.current) setAwayFromTip(!isNearBottom(node));
|
|
2380
|
+
scheduleQuestionNav();
|
|
1568
2381
|
const previousTop = lastScrollTopRef.current;
|
|
1569
2382
|
const previousMaxScroll = lastMaxScrollRef.current;
|
|
1570
2383
|
const nextTop = node.scrollTop;
|
|
@@ -1598,6 +2411,19 @@ export function MessageTimeline({
|
|
|
1598
2411
|
return;
|
|
1599
2412
|
}
|
|
1600
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
|
+
|
|
1601
2427
|
if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1602
2428
|
// Fold / composer / SessionChrome: viewport shrink raises maxScroll without
|
|
1603
2429
|
// growing content. Must hit tipFollow before we adopt the new clientHeight
|
|
@@ -1632,7 +2458,7 @@ export function MessageTimeline({
|
|
|
1632
2458
|
// Pointer-dragged scroll-up away from tip. Layout churn never arms this.
|
|
1633
2459
|
if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
|
|
1634
2460
|
clearReaderIntent();
|
|
1635
|
-
|
|
2461
|
+
requestEarlierFromReader();
|
|
1636
2462
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1637
2463
|
return;
|
|
1638
2464
|
}
|
|
@@ -1657,15 +2483,46 @@ export function MessageTimeline({
|
|
|
1657
2483
|
}
|
|
1658
2484
|
|
|
1659
2485
|
syncScrollBaseline(node);
|
|
2486
|
+
if (programmatic) {
|
|
2487
|
+
// Anchor restoration is never permission to resume tip-follow.
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
1660
2490
|
const nearBottom = isNearBottom(node);
|
|
1661
2491
|
|
|
1662
|
-
// Re-pin only when the reader moved toward/at the tip
|
|
1663
|
-
//
|
|
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;
|
|
1664
2510
|
const nextPinned =
|
|
1665
|
-
!hasNewer && nearBottom &&
|
|
2511
|
+
!hasNewer && nearBottom && towardTip > 0.5 && (inserted <= 1 || explicitReturnToBottom);
|
|
1666
2512
|
if (!nextPinned) {
|
|
1667
2513
|
stopFollow();
|
|
1668
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
|
+
}
|
|
1669
2526
|
applyPinned(nextPinned);
|
|
1670
2527
|
// A far-from-bottom scroll while a Jump-to-latest is pending is the reader
|
|
1671
2528
|
// changing their mind: drop the latch, or a stale one (host rejected or
|
|
@@ -1676,8 +2533,15 @@ export function MessageTimeline({
|
|
|
1676
2533
|
wantPinRef.current = false;
|
|
1677
2534
|
}
|
|
1678
2535
|
if (nextPinned) {
|
|
2536
|
+
clearReaderIntent();
|
|
1679
2537
|
return;
|
|
1680
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
|
+
}
|
|
1681
2545
|
if (!olderPrefetchArmedRef.current) {
|
|
1682
2546
|
olderPrefetchArmedRef.current = true;
|
|
1683
2547
|
setOlderPrefetchArmed(true);
|
|
@@ -1693,6 +2557,9 @@ export function MessageTimeline({
|
|
|
1693
2557
|
return;
|
|
1694
2558
|
}
|
|
1695
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();
|
|
1696
2563
|
if (disclosureKeepsUnpinnedRef.current) {
|
|
1697
2564
|
programmaticScrollRef.current = 0;
|
|
1698
2565
|
stopFollow();
|
|
@@ -1707,263 +2574,486 @@ export function MessageTimeline({
|
|
|
1707
2574
|
}
|
|
1708
2575
|
releasePinAfterScrollSettled(node);
|
|
1709
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
|
+
});
|
|
1710
2587
|
|
|
1711
|
-
|
|
1712
|
-
<
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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}
|
|
2609
|
+
>
|
|
2610
|
+
{onAnnotate ? (
|
|
2611
|
+
<Suspense fallback={null}>
|
|
2612
|
+
<TimelineAnnotationSelection
|
|
2613
|
+
rootRef={scrollRef}
|
|
2614
|
+
sources={annotationSources}
|
|
2615
|
+
onAnnotate={onAnnotate}
|
|
2616
|
+
/>
|
|
2617
|
+
</Suspense>
|
|
2618
|
+
) : null}
|
|
2619
|
+
{/* Pinned: anchoring off so the tip-follow camera owns the motion.
|
|
1729
2620
|
Unpinned: native scroll anchoring holds the reader's place. */}
|
|
1730
|
-
<div
|
|
1731
|
-
ref={scrollRef}
|
|
1732
|
-
data-og-timeline-scroller=""
|
|
1733
|
-
data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
|
|
1734
|
-
tabIndex={-1}
|
|
1735
|
-
onScroll={onScroll}
|
|
1736
|
-
onScrollEnd={onScrollEnd}
|
|
1737
|
-
onWheel={onWheel}
|
|
1738
|
-
onPointerDown={onPointerDown}
|
|
1739
|
-
onKeyDown={onKeyDown}
|
|
1740
|
-
style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
|
|
1741
|
-
className={cn(
|
|
1742
|
-
// tabIndex=-1 is programmatic only — never paint a focus ring on
|
|
1743
|
-
// the whole scroller (click + Shift used to flash a blue outline).
|
|
1744
|
-
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-hidden",
|
|
1745
|
-
autoFollow && pinned && !hasNewer
|
|
1746
|
-
? "[overflow-anchor:none]"
|
|
1747
|
-
: "[overflow-anchor:auto]",
|
|
1748
|
-
)}
|
|
1749
|
-
>
|
|
1750
|
-
<div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
1751
|
-
{!groups.length
|
|
1752
|
-
? (emptyState ?? (
|
|
1753
|
-
<p className="py-10 text-center text-og-menu text-og-fg-subtle">
|
|
1754
|
-
No activity yet.
|
|
1755
|
-
</p>
|
|
1756
|
-
))
|
|
1757
|
-
: null}
|
|
1758
|
-
{hasOlder && olderPrefetchArmed ? (
|
|
1759
|
-
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
1760
|
-
// must never shift content (that shift was itself a wobble).
|
|
1761
2621
|
<div
|
|
1762
|
-
ref={
|
|
1763
|
-
data-og-
|
|
1764
|
-
data-og-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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;
|
|
1779
2643
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1810
|
-
data-og-loading-older=""
|
|
1811
|
-
aria-live="polite"
|
|
1812
|
-
className="absolute inset-x-0 top-3 z-10 flex justify-center"
|
|
1813
|
-
>
|
|
1814
|
-
{loadingOlder || loadingOldest ? (
|
|
1815
|
-
<span className={LOADING_CHIP_CLASS}>
|
|
1816
|
-
<span className="og-shimmer-text">
|
|
1817
|
-
{loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
|
|
1818
|
-
</span>
|
|
1819
|
-
</span>
|
|
1820
|
-
) : null}
|
|
1821
|
-
{hasOlder &&
|
|
1822
|
-
!loadingOlder &&
|
|
1823
|
-
!loadingOldest &&
|
|
1824
|
-
(underfillRetryReady || onJumpToStart) ? (
|
|
1825
|
-
<button
|
|
1826
|
-
type="button"
|
|
1827
|
-
data-og-retry={underfillRetryReady || undefined}
|
|
1828
|
-
data-og-jump-to-start={!underfillRetryReady || undefined}
|
|
1829
|
-
onClick={() => {
|
|
1830
|
-
const node = scrollRef.current;
|
|
1831
|
-
if (underfillRetryReady) {
|
|
1832
|
-
if (node && underfillRetryReadyRef.current) {
|
|
1833
|
-
// AnimatePresence retains this handler during
|
|
1834
|
-
// exit. Current authorization plus the exact
|
|
1835
|
-
// attempt check prevent stale dispatch.
|
|
1836
|
-
requestOlderIfUnderfilled(node, underfillSettledAttempt!);
|
|
1837
|
-
}
|
|
1838
|
-
return;
|
|
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
|
+
);
|
|
1839
2673
|
}
|
|
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
|
-
|
|
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;
|
|
1871
2720
|
}}
|
|
1872
|
-
className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
|
|
1873
2721
|
>
|
|
1874
|
-
|
|
1875
|
-
|
|
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>
|
|
1876
2915
|
) : null}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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) {
|
|
1923
3021
|
applyPinned(true);
|
|
1924
|
-
snapToBottom(
|
|
3022
|
+
snapToBottom(node);
|
|
1925
3023
|
}
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
</AnimatePresence>
|
|
1960
|
-
</div>
|
|
1961
|
-
</TooltipProvider>
|
|
1962
|
-
</EntranceAnimationProvider>
|
|
1963
|
-
</TimelineComputeLabelProvider>
|
|
1964
|
-
</SeenActivityIdsProvider>
|
|
1965
|
-
</FoldMemoryProvider>
|
|
1966
|
-
</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>
|
|
1967
3057
|
);
|
|
1968
3058
|
}
|
|
1969
3059
|
|
|
@@ -2001,7 +3091,11 @@ function useStableTimelineGroupKeys(
|
|
|
2001
3091
|
const previousByItemId = new Map<string, KeyedTimelineGroup>();
|
|
2002
3092
|
for (const previous of previousRef.current) {
|
|
2003
3093
|
for (const itemId of timelineGroupItemIds(previous.group)) {
|
|
2004
|
-
|
|
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
|
+
}
|
|
2005
3099
|
}
|
|
2006
3100
|
}
|
|
2007
3101
|
|
|
@@ -2014,7 +3108,24 @@ function useStableTimelineGroupKeys(
|
|
|
2014
3108
|
// Retain only same-kind matches. Activity → turn wrap must NOT keep the
|
|
2015
3109
|
// activity chip's React key: that reused a collapsed TurnSummary and
|
|
2016
3110
|
// skipped the settle beat (insta-collapse / content flash).
|
|
2017
|
-
|
|
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
|
+
) {
|
|
2018
3129
|
retainedGroup = previous;
|
|
2019
3130
|
break;
|
|
2020
3131
|
}
|
|
@@ -2134,8 +3245,14 @@ type TimelineGroupEntryProps = {
|
|
|
2134
3245
|
groupKey: string;
|
|
2135
3246
|
group: TimelineGroup;
|
|
2136
3247
|
nextGroup?: TimelineGroup | undefined;
|
|
3248
|
+
startupDismissed: boolean;
|
|
2137
3249
|
entranceEnabled: boolean;
|
|
2138
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;
|
|
2139
3256
|
context: TimelineGroupEntryContext;
|
|
2140
3257
|
};
|
|
2141
3258
|
|
|
@@ -2145,15 +3262,20 @@ type TimelineGroupEntryContext = {
|
|
|
2145
3262
|
};
|
|
2146
3263
|
|
|
2147
3264
|
type TimelineGroupBehaviorProps = {
|
|
2148
|
-
|
|
3265
|
+
renderMessageActions: MessageTimelineProps["renderMessageActions"];
|
|
3266
|
+
renderMessageText:
|
|
3267
|
+
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
3268
|
+
| undefined;
|
|
2149
3269
|
onOpenSession: MessageTimelineProps["onOpenSession"];
|
|
2150
3270
|
onMemoryClick: MessageTimelineProps["onMemoryClick"];
|
|
2151
3271
|
onReconnect: MessageTimelineProps["onReconnect"];
|
|
3272
|
+
renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"];
|
|
2152
3273
|
resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
|
|
2153
3274
|
toolRegistry: ToolRegistry;
|
|
2154
3275
|
loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
|
|
2155
3276
|
loadRetainedArtifact: MessageTimelineProps["loadRetainedArtifact"];
|
|
2156
3277
|
loadVideoArtifactPlayback: MessageTimelineProps["loadVideoArtifactPlayback"];
|
|
3278
|
+
genieLoading: MessageTimelineProps["genieLoading"];
|
|
2157
3279
|
turnSummary: MessageTimelineProps["turnSummary"];
|
|
2158
3280
|
};
|
|
2159
3281
|
|
|
@@ -2166,10 +3288,13 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
|
2166
3288
|
groupKey,
|
|
2167
3289
|
group,
|
|
2168
3290
|
nextGroup,
|
|
3291
|
+
startupDismissed,
|
|
2169
3292
|
entranceEnabled,
|
|
2170
3293
|
liveEntranceEnabled,
|
|
3294
|
+
noteFolded,
|
|
2171
3295
|
context,
|
|
2172
3296
|
}: TimelineGroupEntryProps) {
|
|
3297
|
+
const reducedMotion = useReducedMotion();
|
|
2173
3298
|
const { behavior, userMessageDisclosureContext } = context;
|
|
2174
3299
|
const contextCompactionCount =
|
|
2175
3300
|
group.kind === "turn"
|
|
@@ -2180,38 +3305,91 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
|
2180
3305
|
nextGroup.item.phase === "compacted"
|
|
2181
3306
|
? 1
|
|
2182
3307
|
: 0;
|
|
2183
|
-
|
|
2184
|
-
<
|
|
2185
|
-
<
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
2195
|
-
}
|
|
2196
|
-
/>
|
|
2197
|
-
</UserMessageDisclosureProvider>
|
|
2198
|
-
</TimelineGroupRenderBoundary>
|
|
2199
|
-
</EntranceAnimationProvider>
|
|
2200
|
-
</div>
|
|
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>
|
|
2201
3319
|
);
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
+
|
|
3380
|
+
// The full loaded window stays mounted, so settled history rows must be cheap
|
|
3381
|
+
// on every commit: the stable-key projection reuses render-equivalent group
|
|
2206
3382
|
// objects, so memo skips them. Changed projection content receives a new group
|
|
2207
3383
|
// object, and behavior/callback changes are separate props, so ordinary
|
|
2208
3384
|
// streaming and host updates still invalidate immediately.
|
|
2209
3385
|
const TimelineGroupView = memo(function TimelineGroupView({
|
|
2210
3386
|
group,
|
|
3387
|
+
renderMessageActions,
|
|
2211
3388
|
renderMessageText,
|
|
2212
3389
|
onOpenSession,
|
|
2213
3390
|
onMemoryClick,
|
|
2214
3391
|
onReconnect,
|
|
3392
|
+
renderAuthNeeded,
|
|
2215
3393
|
resolveProviderLogo,
|
|
2216
3394
|
toolRegistry,
|
|
2217
3395
|
loadRetainedScreenshot,
|
|
@@ -2221,16 +3399,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2221
3399
|
insideTurn = false,
|
|
2222
3400
|
nestClusterChips = false,
|
|
2223
3401
|
foldLiveCluster = false,
|
|
3402
|
+
startupDismissed = false,
|
|
2224
3403
|
trailingAgentText,
|
|
2225
3404
|
contextCompactionCount,
|
|
2226
3405
|
}: {
|
|
2227
3406
|
group: TimelineGroup;
|
|
3407
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2228
3408
|
renderMessageText?:
|
|
2229
3409
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2230
3410
|
| undefined;
|
|
2231
3411
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2232
3412
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
2233
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;
|
|
2234
3416
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2235
3417
|
toolRegistry: ToolRegistry;
|
|
2236
3418
|
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
@@ -2241,6 +3423,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2241
3423
|
behind a neutral chip — the one place activity without an outcome still
|
|
2242
3424
|
folds, bounding the DOM of days-long autonomous turns. */
|
|
2243
3425
|
foldLiveCluster?: boolean;
|
|
3426
|
+
startupDismissed?: boolean;
|
|
2244
3427
|
/** Rendering inside an expanded turn group: the outer chip already owns the
|
|
2245
3428
|
failure surface, so nested chips stay tinted but quiet (no repeated
|
|
2246
3429
|
failure text, no auto-open) — one loud error, N calm sub-expands. */
|
|
@@ -2261,15 +3444,29 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2261
3444
|
/** Secondary chip facet when this fold sits next to a compaction landmark. */
|
|
2262
3445
|
contextCompactionCount?: number | undefined;
|
|
2263
3446
|
}) {
|
|
3447
|
+
const startupDetails = useStartupDetails();
|
|
2264
3448
|
const enter = useEntranceAnimation();
|
|
2265
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]);
|
|
2266
3456
|
const foldMemory = useFoldMemory();
|
|
3457
|
+
const reportLiveFold = useContext(LiveNoteFoldContext);
|
|
2267
3458
|
// Settled (or live-fold) activity clusters get a chip. Inside an expanded
|
|
2268
3459
|
// turn that is the second layer — quiet nested chips under the outer turn
|
|
2269
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.
|
|
2270
3466
|
const activityShouldFold =
|
|
2271
|
-
group.kind === "activity" &&
|
|
2272
|
-
|
|
3467
|
+
group.kind === "activity" &&
|
|
3468
|
+
!containsPresentedImage &&
|
|
3469
|
+
!!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
|
|
2273
3470
|
// Latch live→folded so a top-level shell that was already mounted open can
|
|
2274
3471
|
// start the settle beat without remounting bare rail → wrapper.
|
|
2275
3472
|
const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn);
|
|
@@ -2278,7 +3475,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2278
3475
|
group.kind === "turn" &&
|
|
2279
3476
|
(group.outcome === "failed" ||
|
|
2280
3477
|
timelineGroupContainsAuthNeeded(group) ||
|
|
2281
|
-
|
|
3478
|
+
containsPresentedImage);
|
|
2282
3479
|
// activity-* → turn-* remount: carry resting state so settleFold does not
|
|
2283
3480
|
// re-open a chip the reader already watched collapse.
|
|
2284
3481
|
if (group.kind === "turn" && foldMemory && !insideTurn) {
|
|
@@ -2289,9 +3486,167 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2289
3486
|
);
|
|
2290
3487
|
}
|
|
2291
3488
|
const settleFold =
|
|
2292
|
-
|
|
3489
|
+
!turnSummary?.rolling &&
|
|
3490
|
+
(group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
|
|
2293
3491
|
switch (group.kind) {
|
|
2294
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
|
+
}
|
|
2295
3650
|
if (insideTurn) {
|
|
2296
3651
|
// Nested chips whenever the parent has ≥2 clusters. During outer settle
|
|
2297
3652
|
// chrome they stay force-open so structure is visible and height stays
|
|
@@ -2303,11 +3658,22 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2303
3658
|
// settled closed pre-wrap was showing as a CHIP, so mounting it closed
|
|
2304
3659
|
// is both the stable height and the honest state — force-opening it
|
|
2305
3660
|
// was the "already-collapsed cluster auto-expands at the end" reopen.
|
|
2306
|
-
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;
|
|
2307
3672
|
if (!useNestedChip) {
|
|
2308
3673
|
return (
|
|
2309
3674
|
<ActivityRail
|
|
2310
3675
|
items={group.items}
|
|
3676
|
+
{...(startupDismissed ? { startupActive: false } : {})}
|
|
2311
3677
|
onOpenSession={onOpenSession}
|
|
2312
3678
|
onMemoryClick={onMemoryClick}
|
|
2313
3679
|
toolRegistry={toolRegistry}
|
|
@@ -2345,10 +3711,31 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2345
3711
|
// flips settleFold (collapse) instead of remounting bare rail → wrapper.
|
|
2346
3712
|
return (
|
|
2347
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"}
|
|
2348
3717
|
items={group.items}
|
|
2349
3718
|
outcome={group.outcome}
|
|
2350
3719
|
failureText={group.failureText}
|
|
2351
|
-
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
|
+
}
|
|
2352
3739
|
foldKey={group.id}
|
|
2353
3740
|
facets={turnSummary?.facets}
|
|
2354
3741
|
settleFold={settleFold}
|
|
@@ -2371,54 +3758,40 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2371
3758
|
);
|
|
2372
3759
|
case "turn": {
|
|
2373
3760
|
const activityItems = flattenActivityItems(group.groups);
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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 />;
|
|
2378
3773
|
const turnCopyText = collectTurnCopyText(group.groups, trailingAgentText);
|
|
2379
|
-
const body = group.groups
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
loadRetainedScreenshot,
|
|
2393
|
-
loadRetainedArtifact,
|
|
2394
|
-
loadVideoArtifactPlayback,
|
|
2395
|
-
turnSummary,
|
|
2396
|
-
]}
|
|
2397
|
-
>
|
|
2398
|
-
<TimelineGroupView
|
|
2399
|
-
group={child}
|
|
2400
|
-
renderMessageText={renderMessageText}
|
|
2401
|
-
onOpenSession={onOpenSession}
|
|
2402
|
-
onMemoryClick={onMemoryClick}
|
|
2403
|
-
onReconnect={onReconnect}
|
|
2404
|
-
resolveProviderLogo={resolveProviderLogo}
|
|
2405
|
-
toolRegistry={toolRegistry}
|
|
2406
|
-
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
2407
|
-
loadRetainedArtifact={loadRetainedArtifact}
|
|
2408
|
-
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
2409
|
-
turnSummary={turnSummary}
|
|
2410
|
-
insideTurn
|
|
2411
|
-
nestClusterChips={nestClusters}
|
|
2412
|
-
/>
|
|
2413
|
-
</TimelineGroupRenderBoundary>
|
|
2414
|
-
);
|
|
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,
|
|
2415
3787
|
});
|
|
3788
|
+
const durationMs = durationBetween(group.startedAt, group.endedAt);
|
|
2416
3789
|
return (
|
|
2417
3790
|
<TurnSummary
|
|
2418
3791
|
items={activityItems}
|
|
2419
3792
|
outcome={group.outcome}
|
|
2420
3793
|
failureText={insideTurn ? undefined : group.failureText}
|
|
2421
|
-
durationMs={
|
|
3794
|
+
durationMs={durationMs}
|
|
2422
3795
|
defaultOpen={turnDefaultOpen ? true : undefined}
|
|
2423
3796
|
bare={insideTurn}
|
|
2424
3797
|
foldKey={group.id}
|
|
@@ -2443,8 +3816,10 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2443
3816
|
return (
|
|
2444
3817
|
<TimelineRow
|
|
2445
3818
|
item={group.item}
|
|
3819
|
+
renderMessageActions={renderMessageActions}
|
|
2446
3820
|
renderMessageText={renderMessageText}
|
|
2447
3821
|
onReconnect={onReconnect}
|
|
3822
|
+
renderAuthNeeded={renderAuthNeeded}
|
|
2448
3823
|
resolveProviderLogo={resolveProviderLogo}
|
|
2449
3824
|
onOpenSession={onOpenSession}
|
|
2450
3825
|
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
@@ -2453,6 +3828,94 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2453
3828
|
}
|
|
2454
3829
|
});
|
|
2455
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
|
+
|
|
2456
3919
|
/**
|
|
2457
3920
|
* Body under a turn/activity chip. Remount flashes are gated by the timeline
|
|
2458
3921
|
* seen-activity-id map (not by killing entrance): FoldBody used to force
|
|
@@ -2464,9 +3927,16 @@ function FoldBody({ children }: { children: ReactNode }) {
|
|
|
2464
3927
|
|
|
2465
3928
|
/** Stable left rule for turn/activity bodies — always present so settle wrap
|
|
2466
3929
|
never inserts or removes the rail chrome. */
|
|
2467
|
-
function TurnRailFrame({ children }: { children: ReactNode }) {
|
|
3930
|
+
function TurnRailFrame({ children, compact = false }: { children: ReactNode; compact?: boolean }) {
|
|
2468
3931
|
return (
|
|
2469
|
-
<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>
|
|
2470
3940
|
);
|
|
2471
3941
|
}
|
|
2472
3942
|
|
|
@@ -2513,26 +3983,15 @@ function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean {
|
|
|
2513
3983
|
}
|
|
2514
3984
|
}
|
|
2515
3985
|
|
|
2516
|
-
/** Generated images are primary user-visible output, not incidental activity. */
|
|
2517
|
-
function timelineGroupContainsGeneratedImage(group: TimelineGroup): boolean {
|
|
2518
|
-
switch (group.kind) {
|
|
2519
|
-
case "item":
|
|
2520
|
-
return false;
|
|
2521
|
-
case "activity":
|
|
2522
|
-
return group.items.some(
|
|
2523
|
-
(item) => item.kind === "tool-call" && item.name === "generate_image",
|
|
2524
|
-
);
|
|
2525
|
-
case "turn":
|
|
2526
|
-
return group.groups.some(timelineGroupContainsGeneratedImage);
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
3986
|
function timelineGroupItemIds(group: TimelineGroup): string[] {
|
|
2531
3987
|
switch (group.kind) {
|
|
2532
3988
|
case "item":
|
|
2533
3989
|
return [group.item.id];
|
|
2534
3990
|
case "activity":
|
|
2535
|
-
return
|
|
3991
|
+
return [
|
|
3992
|
+
...(group.work?.details ?? []).flatMap(timelineGroupItemIds),
|
|
3993
|
+
...group.items.map((item) => item.id),
|
|
3994
|
+
];
|
|
2536
3995
|
case "turn":
|
|
2537
3996
|
return group.groups.flatMap(timelineGroupItemIds);
|
|
2538
3997
|
}
|
|
@@ -2550,25 +4009,13 @@ function isAgentProgress(next: TimelineGroup | undefined): boolean {
|
|
|
2550
4009
|
return (
|
|
2551
4010
|
next.kind === "activity" ||
|
|
2552
4011
|
next.kind === "turn" ||
|
|
2553
|
-
(next.kind === "item" && next.item.kind === "agent-message")
|
|
4012
|
+
(next.kind === "item" && next.item.kind === "agent-message" && next.item.text.trim().length > 0)
|
|
2554
4013
|
);
|
|
2555
4014
|
}
|
|
2556
4015
|
|
|
2557
4016
|
/** No item still running or streaming — the only state safe to fold live.
|
|
2558
4017
|
Position alone is a broken proxy: a pending queued message (or any trailing
|
|
2559
4018
|
item) can sit after the ACTIVE cluster, which must never fold mid-work. */
|
|
2560
|
-
function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>): boolean {
|
|
2561
|
-
return group.items.every((item) => {
|
|
2562
|
-
if (item.kind === "reasoning") {
|
|
2563
|
-
return !item.streaming;
|
|
2564
|
-
}
|
|
2565
|
-
// Memory writes and fleet observations are discrete, already-settled events.
|
|
2566
|
-
if (item.kind === "memory" || item.kind === "fleet-decision") {
|
|
2567
|
-
return true;
|
|
2568
|
-
}
|
|
2569
|
-
return item.status !== "running";
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
4019
|
|
|
2573
4020
|
/** Settled activity clusters that could become nested chips under a turn. */
|
|
2574
4021
|
function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number {
|
|
@@ -2581,18 +4028,6 @@ function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number
|
|
|
2581
4028
|
return count;
|
|
2582
4029
|
}
|
|
2583
4030
|
|
|
2584
|
-
function flattenActivityItems(groups: TimelineGroup[]): ActivityItem[] {
|
|
2585
|
-
const items: ActivityItem[] = [];
|
|
2586
|
-
for (const group of groups) {
|
|
2587
|
-
if (group.kind === "activity") {
|
|
2588
|
-
items.push(...group.items);
|
|
2589
|
-
} else if (group.kind === "turn") {
|
|
2590
|
-
items.push(...flattenActivityItems(group.groups));
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
return items;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
4031
|
/** Assistant prose inside a turn fold (mid-turn narration), joined for copy. */
|
|
2597
4032
|
function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
|
|
2598
4033
|
const parts: string[] = [];
|
|
@@ -2602,6 +4037,18 @@ function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
|
|
|
2602
4037
|
if (text.length > 0) {
|
|
2603
4038
|
parts.push(text);
|
|
2604
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
|
+
}
|
|
2605
4052
|
} else if (group.kind === "turn") {
|
|
2606
4053
|
const nested = collectAgentMessageText(group.groups);
|
|
2607
4054
|
if (nested.length > 0) {
|
|
@@ -2622,6 +4069,9 @@ function collectTurnIdsFromGroups(groups: readonly TimelineGroup[]): Set<string>
|
|
|
2622
4069
|
ids.add(turnId);
|
|
2623
4070
|
}
|
|
2624
4071
|
} else if (group.kind === "activity") {
|
|
4072
|
+
for (const nested of collectTurnIdsFromGroups(group.work?.details ?? [])) {
|
|
4073
|
+
ids.add(nested);
|
|
4074
|
+
}
|
|
2625
4075
|
for (const item of group.items) {
|
|
2626
4076
|
if (item.turnId) {
|
|
2627
4077
|
ids.add(item.turnId);
|
|
@@ -2676,15 +4126,6 @@ function collectTurnCopyText(
|
|
|
2676
4126
|
return parts.join("\n\n");
|
|
2677
4127
|
}
|
|
2678
4128
|
|
|
2679
|
-
function durationBetween(startedAt: string, endedAt: string): number | undefined {
|
|
2680
|
-
const started = Date.parse(startedAt);
|
|
2681
|
-
const ended = Date.parse(endedAt);
|
|
2682
|
-
if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
|
|
2683
|
-
return undefined;
|
|
2684
|
-
}
|
|
2685
|
-
return ended - started;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
4129
|
/* --- single rows ------------------------------------------------------------ */
|
|
2689
4130
|
|
|
2690
4131
|
/**
|
|
@@ -2694,28 +4135,45 @@ function durationBetween(startedAt: string, endedAt: string): number | undefined
|
|
|
2694
4135
|
*/
|
|
2695
4136
|
export function TimelineRow({
|
|
2696
4137
|
item,
|
|
4138
|
+
renderMessageActions,
|
|
2697
4139
|
renderMessageText,
|
|
2698
4140
|
onReconnect,
|
|
4141
|
+
renderAuthNeeded,
|
|
2699
4142
|
resolveProviderLogo,
|
|
2700
4143
|
onOpenSession,
|
|
2701
4144
|
loadVideoArtifactPlayback,
|
|
2702
4145
|
}: {
|
|
2703
4146
|
item: TimelineItem;
|
|
4147
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2704
4148
|
renderMessageText?:
|
|
2705
4149
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2706
4150
|
| undefined;
|
|
2707
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;
|
|
2708
4154
|
resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
|
|
2709
4155
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
2710
4156
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
2711
4157
|
}) {
|
|
2712
4158
|
switch (item.kind) {
|
|
2713
4159
|
case "user-message":
|
|
2714
|
-
return
|
|
4160
|
+
return (
|
|
4161
|
+
<UserMessageRow
|
|
4162
|
+
item={item}
|
|
4163
|
+
renderMessageActions={renderMessageActions}
|
|
4164
|
+
renderMessageText={renderMessageText}
|
|
4165
|
+
/>
|
|
4166
|
+
);
|
|
2715
4167
|
case "human-input":
|
|
2716
4168
|
return <HumanInputConversationRow item={item} />;
|
|
2717
4169
|
case "agent-message":
|
|
2718
|
-
return
|
|
4170
|
+
return (
|
|
4171
|
+
<AgentMessageRow
|
|
4172
|
+
item={item}
|
|
4173
|
+
renderMessageActions={renderMessageActions}
|
|
4174
|
+
renderMessageText={renderMessageText}
|
|
4175
|
+
/>
|
|
4176
|
+
);
|
|
2719
4177
|
case "worker-completion":
|
|
2720
4178
|
return <WorkerCompletionRow item={item} onOpenSession={onOpenSession} />;
|
|
2721
4179
|
case "session-status":
|
|
@@ -2724,7 +4182,11 @@ export function TimelineRow({
|
|
|
2724
4182
|
return <GoalRow item={item} />;
|
|
2725
4183
|
case "machine-input-batch":
|
|
2726
4184
|
return (
|
|
2727
|
-
<MachineInputBatchRow
|
|
4185
|
+
<MachineInputBatchRow
|
|
4186
|
+
item={item}
|
|
4187
|
+
onOpenSession={onOpenSession}
|
|
4188
|
+
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
4189
|
+
/>
|
|
2728
4190
|
);
|
|
2729
4191
|
case "notice":
|
|
2730
4192
|
return <NoticeRow item={item} />;
|
|
@@ -2732,11 +4194,13 @@ export function TimelineRow({
|
|
|
2732
4194
|
return <CompactionRow item={item} />;
|
|
2733
4195
|
case "auth-needed":
|
|
2734
4196
|
return (
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
4197
|
+
renderAuthNeeded?.(item) ?? (
|
|
4198
|
+
<AuthNeededRow
|
|
4199
|
+
item={item}
|
|
4200
|
+
onReconnect={onReconnect}
|
|
4201
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
4202
|
+
/>
|
|
4203
|
+
)
|
|
2740
4204
|
);
|
|
2741
4205
|
default:
|
|
2742
4206
|
return null;
|
|
@@ -2752,6 +4216,8 @@ const COMPACTION_TRIGGER_LABEL: Record<NonNullable<ContextCompactionItem["trigge
|
|
|
2752
4216
|
|
|
2753
4217
|
function CompactionRow({ item }: { item: ContextCompactionItem }) {
|
|
2754
4218
|
const enter = useEntranceAnimation();
|
|
4219
|
+
const detailsId = useId();
|
|
4220
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
2755
4221
|
const trigger =
|
|
2756
4222
|
item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL[item.trigger] : null;
|
|
2757
4223
|
const before =
|
|
@@ -2769,56 +4235,97 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
|
|
|
2769
4235
|
? before && after
|
|
2770
4236
|
? `Conversation history compacted · ~${before} → ~${after} estimated history tokens`
|
|
2771
4237
|
: "Conversation history compacted"
|
|
2772
|
-
:
|
|
4238
|
+
: compactionSkipTitle(item.skipReason);
|
|
2773
4239
|
const subtitle =
|
|
2774
4240
|
item.phase === "compacted"
|
|
2775
4241
|
? "Chat history above is unchanged"
|
|
2776
4242
|
: item.phase === "skipped"
|
|
2777
|
-
? compactionSkipSubtitle(item.skipReason)
|
|
4243
|
+
? compactionSkipSubtitle(item.skipReason, item.providerRejection)
|
|
2778
4244
|
: null;
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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;
|
|
2785
4253
|
return (
|
|
2786
|
-
<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")}>
|
|
2787
4255
|
<div
|
|
2788
4256
|
className={cn(
|
|
2789
|
-
"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",
|
|
2790
4261
|
pill,
|
|
2791
4262
|
)}
|
|
2792
4263
|
role="status"
|
|
2793
4264
|
>
|
|
2794
|
-
<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">
|
|
2795
4266
|
<ShrinkIcon className="size-3.5 shrink-0" />
|
|
2796
|
-
<span className="truncate">
|
|
4267
|
+
<span className="min-w-0 truncate">
|
|
2797
4268
|
{title}
|
|
2798
4269
|
{trigger ? ` · ${trigger}` : ""}
|
|
2799
4270
|
</span>
|
|
2800
4271
|
</span>
|
|
2801
|
-
{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}
|
|
2802
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}
|
|
2803
4325
|
</div>
|
|
2804
4326
|
);
|
|
2805
4327
|
}
|
|
2806
4328
|
|
|
2807
|
-
function compactionSkipSubtitle(reason: string | null): string {
|
|
2808
|
-
switch (reason) {
|
|
2809
|
-
case "no_history":
|
|
2810
|
-
return "No active history to compact";
|
|
2811
|
-
case "replacement_not_smaller":
|
|
2812
|
-
return "Checkpoint would not reduce memory size";
|
|
2813
|
-
case "replacement_unchanged":
|
|
2814
|
-
return "Checkpoint made no progress";
|
|
2815
|
-
case "summarization_failed":
|
|
2816
|
-
return "Request it again to retry. Chat history is unchanged.";
|
|
2817
|
-
default:
|
|
2818
|
-
return "Compaction was not needed. Chat history is unchanged.";
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
4329
|
/** Hover-reveal clock beside the copy control (sent / finished). */
|
|
2823
4330
|
function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
|
|
2824
4331
|
return (
|
|
@@ -2837,9 +4344,11 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
|
|
|
2837
4344
|
|
|
2838
4345
|
function UserMessageRow({
|
|
2839
4346
|
item,
|
|
4347
|
+
renderMessageActions,
|
|
2840
4348
|
renderMessageText,
|
|
2841
4349
|
}: {
|
|
2842
4350
|
item: UserMessageItem;
|
|
4351
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
2843
4352
|
renderMessageText?:
|
|
2844
4353
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
2845
4354
|
| undefined;
|
|
@@ -2849,6 +4358,7 @@ function UserMessageRow({
|
|
|
2849
4358
|
return (
|
|
2850
4359
|
<div className={cn(enter && "animate-og-enter", "flex justify-end")}>
|
|
2851
4360
|
<div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
|
|
4361
|
+
<MessageSenderSlot item={item} />
|
|
2852
4362
|
<CopyHoverFrame
|
|
2853
4363
|
copyText={
|
|
2854
4364
|
item.text ||
|
|
@@ -2858,22 +4368,30 @@ function UserMessageRow({
|
|
|
2858
4368
|
}
|
|
2859
4369
|
label="Copy message"
|
|
2860
4370
|
className="w-fit max-w-full min-w-0"
|
|
2861
|
-
trailing={
|
|
4371
|
+
trailing={
|
|
4372
|
+
<>
|
|
4373
|
+
{renderMessageActions?.(item)}
|
|
4374
|
+
<MessageFooterTime occurredAt={item.occurredAt} />
|
|
4375
|
+
</>
|
|
4376
|
+
}
|
|
2862
4377
|
>
|
|
2863
4378
|
<div className={MESSAGE_BUBBLE_CLASS}>
|
|
2864
4379
|
{item.text ? (
|
|
2865
|
-
<div
|
|
4380
|
+
<div
|
|
4381
|
+
data-og-search-item={item.id}
|
|
4382
|
+
data-og-annotation-source-key={item.annotationSource?.eventId}
|
|
4383
|
+
>
|
|
2866
4384
|
{renderMessageText ? (
|
|
2867
4385
|
renderMessageText(item.text, item)
|
|
2868
4386
|
) : (
|
|
2869
4387
|
<UserMessageBody messageId={item.id} text={item.text}>
|
|
2870
|
-
<Markdown>{item.text}</Markdown>
|
|
4388
|
+
<Markdown softLineBreaks>{item.text}</Markdown>
|
|
2871
4389
|
</UserMessageBody>
|
|
2872
4390
|
)}
|
|
2873
4391
|
</div>
|
|
2874
4392
|
) : null}
|
|
2875
4393
|
{(item.annotations?.length ?? 0) > 0 ? (
|
|
2876
|
-
<
|
|
4394
|
+
<TimelineAnnotationCards
|
|
2877
4395
|
annotations={item.annotations ?? []}
|
|
2878
4396
|
className={item.text ? "mt-2" : undefined}
|
|
2879
4397
|
/>
|
|
@@ -2883,30 +4401,43 @@ function UserMessageRow({
|
|
|
2883
4401
|
{deliveryFailed ? (
|
|
2884
4402
|
<div
|
|
2885
4403
|
role="status"
|
|
2886
|
-
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"
|
|
2887
4405
|
>
|
|
2888
|
-
<span className="
|
|
4406
|
+
<span className="flex min-w-0 items-start gap-1">
|
|
2889
4407
|
<TriangleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
2890
|
-
<span
|
|
4408
|
+
<span className="min-w-0 break-words">
|
|
4409
|
+
{item.delivery?.error || "Message not sent"}
|
|
4410
|
+
</span>
|
|
2891
4411
|
</span>
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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>
|
|
2910
4441
|
</div>
|
|
2911
4442
|
) : null}
|
|
2912
4443
|
</div>
|
|
@@ -2946,7 +4477,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2946
4477
|
item.questions.map((question, index) => (
|
|
2947
4478
|
<div key={question.id}>
|
|
2948
4479
|
{question.label ? (
|
|
2949
|
-
<p className="text-og-sm font-semibold text-og-fg">
|
|
4480
|
+
<p className="text-og-sm font-semibold text-og-fg [overflow-wrap:anywhere]">
|
|
2950
4481
|
{multipleQuestions ? (
|
|
2951
4482
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
2952
4483
|
) : null}
|
|
@@ -2956,7 +4487,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2956
4487
|
) : null}
|
|
2957
4488
|
<p
|
|
2958
4489
|
className={cn(
|
|
2959
|
-
"text-og-md leading-6 text-og-fg",
|
|
4490
|
+
"text-og-md leading-6 text-og-fg [overflow-wrap:anywhere]",
|
|
2960
4491
|
question.label && "mt-0.5 text-og-sm text-og-fg-muted",
|
|
2961
4492
|
)}
|
|
2962
4493
|
>
|
|
@@ -2964,7 +4495,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2964
4495
|
<span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
|
|
2965
4496
|
) : null}
|
|
2966
4497
|
{!question.label && multipleQuestions ? " " : null}
|
|
2967
|
-
{question.prompt}
|
|
4498
|
+
{linkifyText(question.prompt)}
|
|
2968
4499
|
</p>
|
|
2969
4500
|
</div>
|
|
2970
4501
|
))
|
|
@@ -2998,7 +4529,12 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
|
|
|
2998
4529
|
</p>
|
|
2999
4530
|
) : null}
|
|
3000
4531
|
<div className={cn("text-og-md text-og-fg", multipleQuestions && "mt-0.5")}>
|
|
3001
|
-
{answer.
|
|
4532
|
+
{answer.secret ? (
|
|
4533
|
+
<SecretAnswerChip
|
|
4534
|
+
result={answer.secret}
|
|
4535
|
+
label={secretAnswerLabel(answer.secret)}
|
|
4536
|
+
/>
|
|
4537
|
+
) : answer.values.length > 1 ? (
|
|
3002
4538
|
<ul className="list-disc space-y-0.5 pl-5">
|
|
3003
4539
|
{answer.values.map((value) => (
|
|
3004
4540
|
<li key={`${answer.questionId}-${value}`}>{value}</li>
|
|
@@ -3039,7 +4575,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
|
|
|
3039
4575
|
item.questions.length > 1
|
|
3040
4576
|
? `${questionNumberById.get(answer.questionId) ?? index + 1}. `
|
|
3041
4577
|
: ""
|
|
3042
|
-
}${answer.label}: ${
|
|
4578
|
+
}${answer.label}: ${
|
|
4579
|
+
answer.secret
|
|
4580
|
+
? `${secretAnswerLabel(answer.secret)} (${answer.secret.reference})`
|
|
4581
|
+
: answer.values.join(", ") || "Answered"
|
|
4582
|
+
}`,
|
|
3043
4583
|
)
|
|
3044
4584
|
.join("\n\n");
|
|
3045
4585
|
return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`]
|
|
@@ -3049,9 +4589,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
|
|
|
3049
4589
|
|
|
3050
4590
|
function AgentMessageRow({
|
|
3051
4591
|
item,
|
|
4592
|
+
renderMessageActions,
|
|
3052
4593
|
renderMessageText,
|
|
3053
4594
|
}: {
|
|
3054
4595
|
item: AgentMessageItem;
|
|
4596
|
+
renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
3055
4597
|
renderMessageText?:
|
|
3056
4598
|
| ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
|
|
3057
4599
|
| undefined;
|
|
@@ -3062,76 +4604,80 @@ function AgentMessageRow({
|
|
|
3062
4604
|
const body = renderMessageText ? (
|
|
3063
4605
|
renderMessageText(item.text, item)
|
|
3064
4606
|
) : (
|
|
3065
|
-
<Markdown streaming={item.streaming}>
|
|
4607
|
+
<Markdown softLineBreaks streaming={item.streaming}>
|
|
4608
|
+
{item.text}
|
|
4609
|
+
</Markdown>
|
|
3066
4610
|
);
|
|
3067
4611
|
// While streaming, copy is still useful (current text) but keep chrome calm —
|
|
3068
4612
|
// stamp only after the message finishes (occurredAt tracks completion).
|
|
3069
4613
|
return (
|
|
3070
|
-
<
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
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}
|
|
3077
4632
|
>
|
|
3078
4633
|
{body}
|
|
3079
|
-
</
|
|
3080
|
-
</
|
|
4634
|
+
</div>
|
|
4635
|
+
</CopyHoverFrame>
|
|
3081
4636
|
);
|
|
3082
4637
|
}
|
|
3083
4638
|
|
|
3084
4639
|
/**
|
|
3085
|
-
* A worker session reporting back to its manager
|
|
3086
|
-
* as a `user.message` carrying a `childCompletion` payload
|
|
3087
|
-
*
|
|
3088
|
-
*
|
|
3089
|
-
*
|
|
3090
|
-
* full report, evidence, and any paused reason live behind a collapsed
|
|
3091
|
-
* disclosure, and a "View session" affordance deep-links into the child.
|
|
3092
|
-
*
|
|
3093
|
-
* Color follows the timeline's restraint: green only for a completed goal, the
|
|
3094
|
-
* waiting hue only for a paused one, red only for a failed child — everything
|
|
3095
|
-
* 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.
|
|
3096
4645
|
*/
|
|
3097
4646
|
type WorkerCompletionMeta = {
|
|
3098
|
-
|
|
4647
|
+
title: AgentTitleParts;
|
|
3099
4648
|
icon: ComponentType<{ className?: string }>;
|
|
3100
|
-
|
|
3101
|
-
/** The 2px left-accent hue — color spent only on the exceptional outcomes. */
|
|
3102
|
-
accentClass: string;
|
|
4649
|
+
failed: boolean;
|
|
3103
4650
|
};
|
|
3104
4651
|
|
|
3105
|
-
function workerCompletionMeta(
|
|
4652
|
+
function workerCompletionMeta(
|
|
4653
|
+
item: WorkerCompletionItem,
|
|
4654
|
+
name: string | null,
|
|
4655
|
+
): WorkerCompletionMeta {
|
|
3106
4656
|
if (item.childStatus === "failed") {
|
|
3107
4657
|
return {
|
|
3108
|
-
|
|
4658
|
+
title: namedAgentTitle(name, "", " failed", "Worker failed"),
|
|
3109
4659
|
icon: XCircleIcon,
|
|
3110
|
-
|
|
3111
|
-
accentClass: "border-og-status-failed/60",
|
|
4660
|
+
failed: true,
|
|
3112
4661
|
};
|
|
3113
4662
|
}
|
|
3114
4663
|
if (item.goalStatus === "paused") {
|
|
3115
4664
|
return {
|
|
3116
|
-
|
|
4665
|
+
title: namedAgentTitle(name, "", " paused", "Worker paused"),
|
|
3117
4666
|
icon: PauseCircleIcon,
|
|
3118
|
-
|
|
3119
|
-
accentClass: "border-og-status-waiting/50",
|
|
4667
|
+
failed: false,
|
|
3120
4668
|
};
|
|
3121
4669
|
}
|
|
3122
4670
|
if (item.goalStatus === "completed") {
|
|
3123
4671
|
return {
|
|
3124
|
-
|
|
4672
|
+
title: namedAgentTitle(name, "", " completed its goal", "Worker completed"),
|
|
3125
4673
|
icon: CheckCircle2Icon,
|
|
3126
|
-
|
|
3127
|
-
accentClass: "border-og-status-idle/45",
|
|
4674
|
+
failed: false,
|
|
3128
4675
|
};
|
|
3129
4676
|
}
|
|
3130
4677
|
return {
|
|
3131
|
-
|
|
4678
|
+
title: namedAgentTitle(name, "Result from ", "", "Worker reported back"),
|
|
3132
4679
|
icon: BotIcon,
|
|
3133
|
-
|
|
3134
|
-
accentClass: "border-og-border-strong",
|
|
4680
|
+
failed: false,
|
|
3135
4681
|
};
|
|
3136
4682
|
}
|
|
3137
4683
|
|
|
@@ -3143,14 +4689,12 @@ function WorkerCompletionRow({
|
|
|
3143
4689
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3144
4690
|
}) {
|
|
3145
4691
|
const enter = useEntranceAnimation();
|
|
3146
|
-
const
|
|
3147
|
-
const meta = workerCompletionMeta(item);
|
|
4692
|
+
const identity = useAgentIdentity();
|
|
4693
|
+
const meta = workerCompletionMeta(item, identity.titleFor(item.childSessionId));
|
|
3148
4694
|
const Icon = meta.icon;
|
|
3149
|
-
// The worker's own report is the substance behind the fold; evidence and any
|
|
3150
|
-
// paused reason sit alongside it as quieter, labelled context.
|
|
3151
4695
|
// "Paused because" only when the outcome actually IS a pause — completion
|
|
3152
4696
|
// payloads can carry a leftover pausedReason/rationale from earlier in the
|
|
3153
|
-
// worker's life, and a
|
|
4697
|
+
// worker's life, and a completed row must not show a pause section.
|
|
3154
4698
|
const showPausedReason =
|
|
3155
4699
|
item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim();
|
|
3156
4700
|
const details: { label: string; value: string; muted?: boolean }[] = [
|
|
@@ -3162,82 +4706,44 @@ function WorkerCompletionRow({
|
|
|
3162
4706
|
? [{ label: "Paused because", value: item.pausedReason!.trim(), muted: true }]
|
|
3163
4707
|
: []),
|
|
3164
4708
|
];
|
|
3165
|
-
const hasDetails = details.length > 0;
|
|
3166
4709
|
return (
|
|
3167
4710
|
<div className={cn(enter && "animate-og-enter", "min-w-0")}>
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
className={cn(
|
|
3188
|
-
"-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",
|
|
3189
|
-
"outline-hidden transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
|
|
3190
|
-
"focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
3191
|
-
)}
|
|
3192
|
-
>
|
|
3193
|
-
View session
|
|
3194
|
-
<ArrowRightIcon className="size-3.5" />
|
|
3195
|
-
</button>
|
|
3196
|
-
) : null}
|
|
3197
|
-
</div>
|
|
3198
|
-
{hasDetails ? (
|
|
3199
|
-
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
|
3200
|
-
<Collapsible.Trigger asChild>
|
|
3201
|
-
<button
|
|
3202
|
-
type="button"
|
|
3203
|
-
className={cn(
|
|
3204
|
-
"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",
|
|
3205
|
-
"outline-hidden transition-colors duration-150 hover:text-og-fg-muted focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
3206
|
-
)}
|
|
3207
|
-
>
|
|
3208
|
-
<ChevronRightIcon className="size-3 transition-transform duration-150 ease-og-in-out group-data-[state=open]/wc:rotate-90" />
|
|
3209
|
-
{open ? "Hide details" : "Show details"}
|
|
3210
|
-
</button>
|
|
3211
|
-
</Collapsible.Trigger>
|
|
3212
|
-
<Collapsible.Content className="overflow-hidden data-[state=closed]:animate-og-collapse data-[state=open]:animate-og-expand">
|
|
3213
|
-
<div className="ml-1 mt-1.5 flex flex-col gap-2.5">
|
|
3214
|
-
{details.map((detail) => (
|
|
3215
|
-
<div key={detail.label} className="min-w-0">
|
|
3216
|
-
<p className="mb-1 text-og-xs font-medium uppercase tracking-[0.08em] text-og-fg-subtle">
|
|
3217
|
-
{detail.label}
|
|
3218
|
-
</p>
|
|
3219
|
-
<p
|
|
3220
|
-
className={cn(
|
|
3221
|
-
"whitespace-pre-wrap break-words text-og-sm leading-6",
|
|
3222
|
-
detail.muted ? "text-og-fg-subtle" : "text-og-fg-muted",
|
|
3223
|
-
)}
|
|
3224
|
-
>
|
|
3225
|
-
{detail.value}
|
|
3226
|
-
</p>
|
|
3227
|
-
</div>
|
|
3228
|
-
))}
|
|
3229
|
-
</div>
|
|
3230
|
-
</Collapsible.Content>
|
|
3231
|
-
</Collapsible.Root>
|
|
3232
|
-
) : null}
|
|
3233
|
-
</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>
|
|
3234
4730
|
</div>
|
|
3235
4731
|
);
|
|
3236
4732
|
}
|
|
3237
4733
|
|
|
3238
|
-
function SessionStatusRow({
|
|
4734
|
+
function SessionStatusRow({
|
|
4735
|
+
item,
|
|
4736
|
+
}: {
|
|
4737
|
+
item: {
|
|
4738
|
+
status: SessionStatus;
|
|
4739
|
+
blocked?: true;
|
|
4740
|
+
occurredAt: string;
|
|
4741
|
+
resolvedAt?: string;
|
|
4742
|
+
};
|
|
4743
|
+
}) {
|
|
3239
4744
|
const enter = useEntranceAnimation();
|
|
3240
|
-
const
|
|
4745
|
+
const status = item.blocked ? "blocked" : item.status;
|
|
4746
|
+
const meta = SESSION_STATUS_META[status];
|
|
3241
4747
|
return (
|
|
3242
4748
|
<div
|
|
3243
4749
|
className={cn(
|
|
@@ -3248,8 +4754,9 @@ function SessionStatusRow({ item }: { item: { status: SessionStatus; occurredAt:
|
|
|
3248
4754
|
>
|
|
3249
4755
|
<span className="h-px flex-1 bg-og-border" />
|
|
3250
4756
|
<span className="inline-flex items-center gap-1.5">
|
|
3251
|
-
<StatusDot status={
|
|
3252
|
-
{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)}
|
|
3253
4760
|
</span>
|
|
3254
4761
|
<span className="h-px flex-1 bg-og-border" />
|
|
3255
4762
|
</div>
|
|
@@ -3277,41 +4784,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
|
|
|
3277
4784
|
|
|
3278
4785
|
const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
|
|
3279
4786
|
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
label: "Goal
|
|
3301
|
-
pill:
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
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
|
+
}
|
|
3306
4818
|
|
|
3307
4819
|
/**
|
|
3308
4820
|
* A goal landmark pill. Resolves its label, accent/tone, and glyph from
|
|
3309
|
-
* {@link
|
|
4821
|
+
* {@link goalMeta} so all six actions are visually distinguishable while the
|
|
3310
4822
|
* palette stays restrained — see that table for the per-action rationale.
|
|
3311
4823
|
*/
|
|
3312
4824
|
function GoalRow({ item }: { item: GoalItem }) {
|
|
3313
4825
|
const enter = useEntranceAnimation();
|
|
3314
|
-
const { label, pill, icon: Icon } =
|
|
4826
|
+
const { label, pill, icon: Icon } = goalMeta(item.action);
|
|
3315
4827
|
return (
|
|
3316
4828
|
<div className={cn(enter && "animate-og-enter", "flex justify-center")}>
|
|
3317
4829
|
<span
|
|
@@ -3332,13 +4844,16 @@ function GoalRow({ item }: { item: GoalItem }) {
|
|
|
3332
4844
|
|
|
3333
4845
|
function MachineInputBatchRow({
|
|
3334
4846
|
item,
|
|
4847
|
+
onOpenSession,
|
|
3335
4848
|
loadVideoArtifactPlayback,
|
|
3336
4849
|
}: {
|
|
3337
4850
|
item: MachineInputBatchItem;
|
|
4851
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3338
4852
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
3339
4853
|
}) {
|
|
3340
4854
|
const enter = useEntranceAnimation();
|
|
3341
|
-
const
|
|
4855
|
+
const identity = useAgentIdentity();
|
|
4856
|
+
const label = machineInputBatchLabel(item.members, identity.titleFor);
|
|
3342
4857
|
const single = item.members.length === 1 ? item.members[0]! : null;
|
|
3343
4858
|
if (single?.kind === "media_generation_result" && single.result) {
|
|
3344
4859
|
return (
|
|
@@ -3349,12 +4864,21 @@ function MachineInputBatchRow({
|
|
|
3349
4864
|
);
|
|
3350
4865
|
}
|
|
3351
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.
|
|
3352
4869
|
const showCollapsedSummary =
|
|
3353
|
-
|
|
4870
|
+
!item.compact &&
|
|
4871
|
+
single != null &&
|
|
4872
|
+
!isAgentMember(single) &&
|
|
4873
|
+
machineInputSummaryIsUseful(single.kind, singleSummary);
|
|
3354
4874
|
|
|
3355
4875
|
return (
|
|
3356
4876
|
<div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
|
|
3357
|
-
<details
|
|
4877
|
+
<details
|
|
4878
|
+
className="group w-full max-w-full"
|
|
4879
|
+
data-og-machine-input-batch=""
|
|
4880
|
+
title={`Received ${formatClockTime(item.occurredAt)}`}
|
|
4881
|
+
>
|
|
3358
4882
|
<summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
|
|
3359
4883
|
<span
|
|
3360
4884
|
className={cn(
|
|
@@ -3369,14 +4893,26 @@ function MachineInputBatchRow({
|
|
|
3369
4893
|
<span className="truncate">{label}</span>
|
|
3370
4894
|
</span>
|
|
3371
4895
|
</summary>
|
|
3372
|
-
<div className="mx-auto mt-2 w-full max-w-lg space-y-
|
|
3373
|
-
|
|
3374
|
-
<
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
+
)}
|
|
3380
4916
|
</div>
|
|
3381
4917
|
</details>
|
|
3382
4918
|
{showCollapsedSummary ? (
|
|
@@ -3388,11 +4924,94 @@ function MachineInputBatchRow({
|
|
|
3388
4924
|
);
|
|
3389
4925
|
}
|
|
3390
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
|
+
|
|
3391
5008
|
function MachineInputRow({
|
|
3392
5009
|
member,
|
|
5010
|
+
onOpenSession,
|
|
3393
5011
|
loadVideoArtifactPlayback,
|
|
3394
5012
|
}: {
|
|
3395
5013
|
member: MachineInputBatchItem["members"][number];
|
|
5014
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
3396
5015
|
loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
|
|
3397
5016
|
}) {
|
|
3398
5017
|
if (member.kind === "media_generation_result" && member.result) {
|
|
@@ -3419,6 +5038,11 @@ function MachineInputRow({
|
|
|
3419
5038
|
{truncate(summary, 320)}
|
|
3420
5039
|
</p>
|
|
3421
5040
|
) : null}
|
|
5041
|
+
<ChildSessionLink
|
|
5042
|
+
kind={member.kind}
|
|
5043
|
+
sourceId={member.sourceId}
|
|
5044
|
+
onOpenSession={onOpenSession}
|
|
5045
|
+
/>
|
|
3422
5046
|
</div>
|
|
3423
5047
|
</div>
|
|
3424
5048
|
);
|
|
@@ -3492,12 +5116,36 @@ function formatVideoDuration(seconds: number): string {
|
|
|
3492
5116
|
|
|
3493
5117
|
function NoticeRow({ item }: { item: NoticeItem }) {
|
|
3494
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);
|
|
3495
5143
|
const tone =
|
|
3496
|
-
|
|
5144
|
+
presentedTone === "failed"
|
|
3497
5145
|
? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
|
|
3498
|
-
:
|
|
5146
|
+
: presentedTone === "waiting"
|
|
3499
5147
|
? WAITING_PILL_CLASS
|
|
3500
|
-
:
|
|
5148
|
+
: NEUTRAL_PILL;
|
|
3501
5149
|
return (
|
|
3502
5150
|
<div
|
|
3503
5151
|
className={cn(
|
|
@@ -3507,16 +5155,22 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
3507
5155
|
)}
|
|
3508
5156
|
role="status"
|
|
3509
5157
|
>
|
|
3510
|
-
|
|
3511
|
-
className=
|
|
3512
|
-
|
|
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
|
+
)}
|
|
3513
5165
|
<div className="min-w-0 flex-1">
|
|
3514
|
-
<span className="whitespace-pre-wrap break-words">{item
|
|
5166
|
+
<span className="whitespace-pre-wrap break-words">{noticeDisplayText(item)}</span>
|
|
3515
5167
|
{item.details ? (
|
|
3516
5168
|
<details className="mt-2 text-og-control">
|
|
3517
5169
|
<summary className="cursor-pointer font-medium">{item.details.label}</summary>
|
|
3518
|
-
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-og-sm bg-
|
|
3519
|
-
{
|
|
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)}
|
|
3520
5174
|
</pre>
|
|
3521
5175
|
</details>
|
|
3522
5176
|
) : null}
|
|
@@ -3535,6 +5189,41 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
3535
5189
|
);
|
|
3536
5190
|
}
|
|
3537
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
|
+
|
|
3538
5227
|
/**
|
|
3539
5228
|
* The inline connection-recovery card: missing or lapsed access surfaces as a
|
|
3540
5229
|
* calm, tappable affordance instead of a raw provider-domain error. The `reason`
|
|
@@ -3581,6 +5270,7 @@ function AuthNeededRow({
|
|
|
3581
5270
|
? `${provider} tools unavailable`
|
|
3582
5271
|
: `${actionLabel} ${provider}`;
|
|
3583
5272
|
const reasonLine = recommendation?.rationale ?? authReasonLine(item.reason);
|
|
5273
|
+
const hostAuthorizationUrl = item.authoritySource === "host" ? item.authorizationUrl : null;
|
|
3584
5274
|
|
|
3585
5275
|
const start = async () => {
|
|
3586
5276
|
if (!onReconnect || busy) {
|
|
@@ -3623,27 +5313,40 @@ function AuthNeededRow({
|
|
|
3623
5313
|
) : null}
|
|
3624
5314
|
</div>
|
|
3625
5315
|
</div>
|
|
3626
|
-
{
|
|
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 ? (
|
|
3627
5330
|
<button
|
|
3628
5331
|
type="button"
|
|
3629
5332
|
onClick={() => void start()}
|
|
3630
5333
|
disabled={busy}
|
|
3631
5334
|
className={cn(
|
|
3632
5335
|
PRIMARY_ACTION_CLASS,
|
|
3633
|
-
"transition-colors hover:bg-og-
|
|
5336
|
+
"transition-colors hover:bg-og-primary-hover disabled:opacity-50 pointer-coarse:min-h-9",
|
|
3634
5337
|
)}
|
|
3635
5338
|
>
|
|
3636
5339
|
<RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
|
|
3637
5340
|
{busy ? "Opening…" : actionLabel}
|
|
3638
5341
|
</button>
|
|
3639
|
-
) : !unavailable && item.authorizationUrl ? (
|
|
5342
|
+
) : item.authoritySource !== "host" && !unavailable && item.authorizationUrl ? (
|
|
3640
5343
|
<a
|
|
3641
5344
|
href={item.authorizationUrl}
|
|
3642
5345
|
rel="noreferrer"
|
|
3643
5346
|
target="_blank"
|
|
3644
5347
|
className={cn(
|
|
3645
5348
|
PRIMARY_ACTION_CLASS,
|
|
3646
|
-
"transition-colors hover:bg-og-
|
|
5349
|
+
"transition-colors hover:bg-og-primary-hover pointer-coarse:min-h-9",
|
|
3647
5350
|
)}
|
|
3648
5351
|
>
|
|
3649
5352
|
<RefreshCwIcon className="size-3.5" aria-hidden />
|