@signalflare-ai/ui 1.0.0 → 1.2.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/CHANGELOG.md +37 -4
- package/README.md +1 -1
- package/ai/component-registry.json +1047 -183
- package/ai/component-registry.md +4241 -50
- package/ai/schemas.ts +99 -502
- package/dist/.build-complete +1 -1
- package/dist/ai/schemas.d.ts +76 -58
- package/dist/ai/schemas.d.ts.map +1 -1
- package/dist/{ai-actions-DSVeQn4e.js → ai-actions-BdUZI3Gk.js} +3 -3
- package/dist/{ai-actions-DSVeQn4e.js.map → ai-actions-BdUZI3Gk.js.map} +1 -1
- package/dist/{ai-agent-card-BXHwhWAU.js → ai-agent-card-BR2NIYhi.js} +1 -1
- package/dist/{ai-agent-card-BXHwhWAU.js.map → ai-agent-card-BR2NIYhi.js.map} +1 -1
- package/dist/{ai-approval-aa0qvjFN.js → ai-approval-Ba7mrKba.js} +2 -2
- package/dist/{ai-approval-aa0qvjFN.js.map → ai-approval-Ba7mrKba.js.map} +1 -1
- package/dist/{ai-code-block-BgtIxtZZ.js → ai-code-block-CZtoL73R.js} +3 -3
- package/dist/{ai-code-block-BgtIxtZZ.js.map → ai-code-block-CZtoL73R.js.map} +1 -1
- package/dist/ai-conversation-Cc7WlaBg.js +242 -0
- package/dist/ai-conversation-Cc7WlaBg.js.map +1 -0
- package/dist/{ai-info-banner-uFxHHwBA.js → ai-info-banner-C7EWPBj7.js} +7 -3
- package/dist/ai-info-banner-C7EWPBj7.js.map +1 -0
- package/dist/{ai-message-BjnFznXy.js → ai-message-Bp7L68U_.js} +27 -8
- package/dist/ai-message-Bp7L68U_.js.map +1 -0
- package/dist/{ai-mission-header-08__gULL.js → ai-mission-header-TiCJfTNt.js} +1 -1
- package/dist/{ai-mission-header-08__gULL.js.map → ai-mission-header-TiCJfTNt.js.map} +1 -1
- package/dist/{ai-part-group-DBtgTgAn.js → ai-part-group-DNb9I446.js} +3 -3
- package/dist/{ai-part-group-DBtgTgAn.js.map → ai-part-group-DNb9I446.js.map} +1 -1
- package/dist/{ai-prompt-input-Dy1LfxPk.js → ai-prompt-input-BVvov_KF.js} +467 -25
- package/dist/ai-prompt-input-BVvov_KF.js.map +1 -0
- package/dist/{ai-question-CHHoDJMg.js → ai-question-GPPMk7YM.js} +2 -2
- package/dist/{ai-question-CHHoDJMg.js.map → ai-question-GPPMk7YM.js.map} +1 -1
- package/dist/{ai-reasoning-CnL6ZSr5.js → ai-reasoning-_feFjk56.js} +2 -2
- package/dist/{ai-reasoning-CnL6ZSr5.js.map → ai-reasoning-_feFjk56.js.map} +1 -1
- package/dist/{ai-response-BEUg3xvd.js → ai-response-CvjV3WhV.js} +8 -3
- package/dist/ai-response-CvjV3WhV.js.map +1 -0
- package/dist/{ai-shimmer-By5_L05p.js → ai-shimmer-j6lKIrjj.js} +1 -1
- package/dist/{ai-shimmer-By5_L05p.js.map → ai-shimmer-j6lKIrjj.js.map} +1 -1
- package/dist/{ai-status-badge-BGYGWYF6.js → ai-status-badge-CSU_QOdz.js} +1 -1
- package/dist/{ai-status-badge-BGYGWYF6.js.map → ai-status-badge-CSU_QOdz.js.map} +1 -1
- package/dist/{ai-streaming-text-CMfoThV0.js → ai-streaming-text-IWW1BhvZ.js} +44 -16
- package/dist/ai-streaming-text-IWW1BhvZ.js.map +1 -0
- package/dist/{ai-subagent-DcPRqkAA.js → ai-subagent-JA4iIMW3.js} +13 -5
- package/dist/ai-subagent-JA4iIMW3.js.map +1 -0
- package/dist/{ai-suggestion-MgeCg5Ar.js → ai-suggestion-BdO6MBuH.js} +2 -2
- package/dist/{ai-suggestion-MgeCg5Ar.js.map → ai-suggestion-BdO6MBuH.js.map} +1 -1
- package/dist/{ai-task-list-Da9zIm00.js → ai-task-list-DYw4R1FA.js} +12 -5
- package/dist/ai-task-list-DYw4R1FA.js.map +1 -0
- package/dist/{ai-timeline-Cwu045IR.js → ai-timeline-C42tOUT8.js} +1 -1
- package/dist/{ai-timeline-Cwu045IR.js.map → ai-timeline-C42tOUT8.js.map} +1 -1
- package/dist/{ai-tool-Cn1O4xjP.js → ai-tool-03jOTwUI.js} +23 -10
- package/dist/ai-tool-03jOTwUI.js.map +1 -0
- package/dist/{ai-usage-bar-DjS12DMp.js → ai-usage-bar-BRf5LC_b.js} +1 -1
- package/dist/{ai-usage-bar-DjS12DMp.js.map → ai-usage-bar-BRf5LC_b.js.map} +1 -1
- package/dist/{badge-D_eaA6wv.js → badge-BheXjMc8.js} +2 -2
- package/dist/{badge-D_eaA6wv.js.map → badge-BheXjMc8.js.map} +1 -1
- package/dist/{banner-B_6oBrsu.js → banner-CcsjunJg.js} +7 -2
- package/dist/banner-CcsjunJg.js.map +1 -0
- package/dist/{breadcrumbs-BlmeYfgq.js → breadcrumbs-CouSyy3H.js} +3 -3
- package/dist/{breadcrumbs-BlmeYfgq.js.map → breadcrumbs-CouSyy3H.js.map} +1 -1
- package/dist/{button-De0267YU.js → button-CO6-qPax.js} +1 -1
- package/dist/{button-De0267YU.js.map → button-CO6-qPax.js.map} +1 -1
- package/dist/catalog.js +1 -1
- package/dist/{chart-BK3sVPnD.js → chart-Dg0qUeSc.js} +2 -2
- package/dist/{chart-BK3sVPnD.js.map → chart-Dg0qUeSc.js.map} +1 -1
- package/dist/{checkbox-DYhUmZNw.js → checkbox-D7p4QKsC.js} +2 -2
- package/dist/{checkbox-DYhUmZNw.js.map → checkbox-D7p4QKsC.js.map} +1 -1
- package/dist/{clipboard-text-ssybngLw.js → clipboard-text-kLaMogs3.js} +3 -3
- package/dist/{clipboard-text-ssybngLw.js.map → clipboard-text-kLaMogs3.js.map} +1 -1
- package/dist/{code-Cx-QSoOT.js → code-BN8InC0G.js} +2 -2
- package/dist/{code-Cx-QSoOT.js.map → code-BN8InC0G.js.map} +1 -1
- package/dist/{collapsible-DWsXeXmS.js → collapsible-D_ueZ0jz.js} +1 -1
- package/dist/{collapsible-DWsXeXmS.js.map → collapsible-D_ueZ0jz.js.map} +1 -1
- package/dist/{combobox-C0iW6a0r.js → combobox-B7TOK0U2.js} +3 -3
- package/dist/{combobox-C0iW6a0r.js.map → combobox-B7TOK0U2.js.map} +1 -1
- package/dist/{command-palette-DGzioeki.js → command-palette-CuNUyJca.js} +2 -2
- package/dist/{command-palette-DGzioeki.js.map → command-palette-CuNUyJca.js.map} +1 -1
- package/dist/components/ai-actions.js +1 -1
- package/dist/components/ai-agent-card.js +1 -1
- package/dist/components/ai-approval.js +1 -1
- package/dist/components/ai-code-block.js +1 -1
- package/dist/components/ai-conversation.js +2 -2
- package/dist/components/ai-info-banner.js +1 -1
- package/dist/components/ai-message.js +1 -1
- package/dist/components/ai-mission-header.js +1 -1
- package/dist/components/ai-part-group.js +1 -1
- package/dist/components/ai-prompt-input.js +2 -2
- package/dist/components/ai-question.js +1 -1
- package/dist/components/ai-reasoning.js +1 -1
- package/dist/components/ai-response.js +1 -1
- package/dist/components/ai-shimmer.js +1 -1
- package/dist/components/ai-status-badge.js +1 -1
- package/dist/components/ai-streaming-text.js +2 -2
- package/dist/components/ai-subagent.js +1 -1
- package/dist/components/ai-suggestion.js +1 -1
- package/dist/components/ai-task-list.js +1 -1
- package/dist/components/ai-timeline.js +1 -1
- package/dist/components/ai-tool.js +1 -1
- package/dist/components/ai-usage-bar.js +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.js +1 -1
- package/dist/components/breadcrumbs.js +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/chart.js +2 -2
- package/dist/components/checkbox.js +1 -1
- package/dist/components/clipboard-text.js +1 -1
- package/dist/components/code.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command-palette.js +1 -1
- package/dist/components/data-grid.js +1 -1
- package/dist/components/date-picker.js +1 -1
- package/dist/components/date-range-picker.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/dropdown.js +1 -1
- package/dist/components/empty.js +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/filters.js +1 -1
- package/dist/components/flow.js +1 -1
- package/dist/components/grid.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/label.js +1 -1
- package/dist/components/layer-card.js +1 -1
- package/dist/components/loader.js +1 -1
- package/dist/components/menubar.js +1 -1
- package/dist/components/meter.js +1 -1
- package/dist/components/pagination.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/radio.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/sensitive-input.js +1 -1
- package/dist/components/sidebar.js +1 -1
- package/dist/components/signalflare-ai-logo.js +1 -1
- package/dist/components/sparkline.js +1 -1
- package/dist/components/stat-card.js +1 -1
- package/dist/components/surface.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/text-roll.js +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/theme-toggle.js +1 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/use-agent-harness.js +1 -1
- package/dist/{data-grid-CG76N_hK.js → data-grid-DGHmU0w3.js} +8 -8
- package/dist/{data-grid-CG76N_hK.js.map → data-grid-DGHmU0w3.js.map} +1 -1
- package/dist/{date-picker-Dqg9L4xu.js → date-picker--ox89RBy.js} +1 -1
- package/dist/{date-picker-Dqg9L4xu.js.map → date-picker--ox89RBy.js.map} +1 -1
- package/dist/{date-range-picker-D75LLINc.js → date-range-picker-DVa7QBqE.js} +1 -1
- package/dist/{date-range-picker-D75LLINc.js.map → date-range-picker-DVa7QBqE.js.map} +1 -1
- package/dist/{dialog-CyHEQXEY.js → dialog-Bv1oSFOd.js} +2 -2
- package/dist/{dialog-CyHEQXEY.js.map → dialog-Bv1oSFOd.js.map} +1 -1
- package/dist/{dist-1-gcEL2L.js → dist-B6iWiWwp.js} +25 -25
- package/dist/{dist-1-gcEL2L.js.map → dist-B6iWiWwp.js.map} +1 -1
- package/dist/{dropdown-qnEYRFXZ.js → dropdown-B_nrGXjV.js} +2 -2
- package/dist/{dropdown-qnEYRFXZ.js.map → dropdown-B_nrGXjV.js.map} +1 -1
- package/dist/{echart-DURZEyai.js → echart-CdOUaT-r.js} +1 -1
- package/dist/{echart-DURZEyai.js.map → echart-CdOUaT-r.js.map} +1 -1
- package/dist/{empty-D2TypIId.js → empty-DZnN0zKX.js} +11 -6
- package/dist/empty-DZnN0zKX.js.map +1 -0
- package/dist/{field-Y_UK1_Cg.js → field-B_yVof52.js} +2 -2
- package/dist/{field-Y_UK1_Cg.js.map → field-B_yVof52.js.map} +1 -1
- package/dist/{filters-Bw_U6ZTx.js → filters-cpJCY21R.js} +7 -7
- package/dist/{filters-Bw_U6ZTx.js.map → filters-cpJCY21R.js.map} +1 -1
- package/dist/{flow-BRsYUCJa.js → flow-B4v198ot.js} +1 -1
- package/dist/{flow-BRsYUCJa.js.map → flow-B4v198ot.js.map} +1 -1
- package/dist/genui.js +1 -1
- package/dist/{grid-qUAN9hFx.js → grid-CEd64Lnh.js} +1 -1
- package/dist/{grid-qUAN9hFx.js.map → grid-CEd64Lnh.js.map} +1 -1
- package/dist/{highlight-to-react-ClEfL81q.js → highlight-to-react-D0Yav4jk.js} +1 -1
- package/dist/{highlight-to-react-ClEfL81q.js.map → highlight-to-react-D0Yav4jk.js.map} +1 -1
- package/dist/index.js +69 -69
- package/dist/{input-DXYUjGgD.js → input-B2bbijRh.js} +2 -2
- package/dist/{input-DXYUjGgD.js.map → input-B2bbijRh.js.map} +1 -1
- package/dist/{input-DddtBN-g.js → input-ClB_E4Lb.js} +4 -4
- package/dist/{input-DddtBN-g.js.map → input-ClB_E4Lb.js.map} +1 -1
- package/dist/{label-QtJxtJ4u.js → label-DUv_urO1.js} +2 -2
- package/dist/{label-QtJxtJ4u.js.map → label-DUv_urO1.js.map} +1 -1
- package/dist/{layer-card-BME0eljh.js → layer-card-BK7eYfwn.js} +1 -1
- package/dist/{layer-card-BME0eljh.js.map → layer-card-BK7eYfwn.js.map} +1 -1
- package/dist/layout-DJHMMap2.js +6103 -0
- package/dist/layout-DJHMMap2.js.map +1 -0
- package/dist/measured-text-BI3dTJmH.js +290 -0
- package/dist/measured-text-BI3dTJmH.js.map +1 -0
- package/dist/{menubar-C8NzAjfd.js → menubar-Cxf3xeAt.js} +2 -2
- package/dist/{menubar-C8NzAjfd.js.map → menubar-Cxf3xeAt.js.map} +1 -1
- package/dist/{meter-CpmTenEr.js → meter-BFFe9l5b.js} +1 -1
- package/dist/{meter-CpmTenEr.js.map → meter-BFFe9l5b.js.map} +1 -1
- package/dist/{pagination-BVqdlONY.js → pagination-yS372Tr4.js} +2 -2
- package/dist/{pagination-BVqdlONY.js.map → pagination-yS372Tr4.js.map} +1 -1
- package/dist/{popover-BRQZ2b6z.js → popover-SRoJaCZr.js} +1 -1
- package/dist/{popover-BRQZ2b6z.js.map → popover-SRoJaCZr.js.map} +1 -1
- package/dist/{radio-BNSwOt3B.js → radio-BcwhwYNB.js} +1 -1
- package/dist/{radio-BNSwOt3B.js.map → radio-BcwhwYNB.js.map} +1 -1
- package/dist/{select-1w2aebGQ.js → select-DMhdoHMa.js} +4 -4
- package/dist/{select-1w2aebGQ.js.map → select-DMhdoHMa.js.map} +1 -1
- package/dist/{sensitive-input-82Cez3vj.js → sensitive-input-CJUpIRal.js} +3 -3
- package/dist/{sensitive-input-82Cez3vj.js.map → sensitive-input-CJUpIRal.js.map} +1 -1
- package/dist/{sidebar-CAsCmSpM.js → sidebar-D4zrlYpn.js} +2 -2
- package/dist/{sidebar-CAsCmSpM.js.map → sidebar-D4zrlYpn.js.map} +1 -1
- package/dist/{signalflare-ai-logo-DDhxMJD6.js → signalflare-ai-logo-Bipogceq.js} +1 -1
- package/dist/{signalflare-ai-logo-DDhxMJD6.js.map → signalflare-ai-logo-Bipogceq.js.map} +1 -1
- package/dist/{skeleton-line-Do3UmGk9.js → skeleton-line-CH1-h6e2.js} +1 -1
- package/dist/{skeleton-line-Do3UmGk9.js.map → skeleton-line-CH1-h6e2.js.map} +1 -1
- package/dist/{sparkline-DdbeM4Ai.js → sparkline-DHmgj1d0.js} +2 -2
- package/dist/{sparkline-DdbeM4Ai.js.map → sparkline-DHmgj1d0.js.map} +1 -1
- package/dist/src/blocks/agent-harness/agent-harness.d.ts.map +1 -1
- package/dist/src/blocks/agent-harness/agent-harness.tsx +29 -5
- package/dist/src/components/ai-conversation/ai-conversation.d.ts +69 -37
- package/dist/src/components/ai-conversation/ai-conversation.d.ts.map +1 -1
- package/dist/src/components/ai-conversation/index.d.ts +2 -1
- package/dist/src/components/ai-conversation/index.d.ts.map +1 -1
- package/dist/src/components/ai-conversation/measurement-constants.d.ts +30 -0
- package/dist/src/components/ai-conversation/measurement-constants.d.ts.map +1 -0
- package/dist/src/components/ai-info-banner/ai-info-banner.d.ts.map +1 -1
- package/dist/src/components/ai-message/ai-message.d.ts +3 -0
- package/dist/src/components/ai-message/ai-message.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/ai-prompt-input.d.ts +58 -4
- package/dist/src/components/ai-prompt-input/ai-prompt-input.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/controller.d.ts +10 -2
- package/dist/src/components/ai-prompt-input/controller.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/index.d.ts +2 -2
- package/dist/src/components/ai-prompt-input/index.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/types.d.ts +16 -0
- package/dist/src/components/ai-prompt-input/types.d.ts.map +1 -1
- package/dist/src/components/ai-response/ai-response.d.ts +12 -1
- package/dist/src/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/src/components/ai-streaming-text/ai-streaming-text.d.ts +27 -0
- package/dist/src/components/ai-streaming-text/ai-streaming-text.d.ts.map +1 -1
- package/dist/src/components/ai-streaming-text/index.d.ts +1 -1
- package/dist/src/components/ai-streaming-text/index.d.ts.map +1 -1
- package/dist/src/components/ai-subagent/ai-subagent.d.ts.map +1 -1
- package/dist/src/components/ai-task-list/ai-task-list.d.ts.map +1 -1
- package/dist/src/components/ai-tool/ai-tool.d.ts.map +1 -1
- package/dist/src/components/banner/banner.d.ts.map +1 -1
- package/dist/src/components/empty/empty.d.ts.map +1 -1
- package/dist/src/components/stat-card/stat-card.d.ts +5 -0
- package/dist/src/components/stat-card/stat-card.d.ts.map +1 -1
- package/dist/src/components/text/text.d.ts +35 -1
- package/dist/src/components/text/text.d.ts.map +1 -1
- package/dist/src/components/tooltip/tooltip.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/utils/index.d.ts +2 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/measured-text.d.ts +40 -0
- package/dist/src/utils/measured-text.d.ts.map +1 -0
- package/dist/src/utils/use-measured-text.d.ts +59 -0
- package/dist/src/utils/use-measured-text.d.ts.map +1 -0
- package/dist/{stat-card-CEZscNh8.js → stat-card-Ew-ofzEm.js} +28 -10
- package/dist/stat-card-Ew-ofzEm.js.map +1 -0
- package/dist/styles/sf-binding.css +9 -1
- package/dist/styles/sf-standalone.css +2 -2
- package/dist/styles/shadcn.css +1 -1
- package/dist/styles/theme-fedramp.css +12 -3
- package/dist/styles/theme-minimal.css +104 -26
- package/dist/styles/theme-sf.css +138 -39
- package/dist/styles/theme-vesper.css +91 -0
- package/dist/{surface-BduI7Ehl.js → surface-DGwRlC0o.js} +1 -1
- package/dist/{surface-BduI7Ehl.js.map → surface-DGwRlC0o.js.map} +1 -1
- package/dist/{switch-CzZBRBL7.js → switch-BxAMfHdt.js} +2 -2
- package/dist/{switch-CzZBRBL7.js.map → switch-BxAMfHdt.js.map} +1 -1
- package/dist/{table-Rv4JMy0B.js → table-BBeAtYVZ.js} +2 -2
- package/dist/{table-Rv4JMy0B.js.map → table-BBeAtYVZ.js.map} +1 -1
- package/dist/{tabs-1cHrYoel.js → tabs-CeHu7Scn.js} +1 -1
- package/dist/{tabs-1cHrYoel.js.map → tabs-CeHu7Scn.js.map} +1 -1
- package/dist/{text-KJmGkwnf.js → text-Cqryz7rk.js} +27 -5
- package/dist/text-Cqryz7rk.js.map +1 -0
- package/dist/{text-roll-BZ3I1umc.js → text-roll-Ch52hcQj.js} +1 -1
- package/dist/{text-roll-BZ3I1umc.js.map → text-roll-Ch52hcQj.js.map} +1 -1
- package/dist/{theme-toggle-Bhu681D7.js → theme-toggle-LDfIKEqx.js} +3 -3
- package/dist/{theme-toggle-Bhu681D7.js.map → theme-toggle-LDfIKEqx.js.map} +1 -1
- package/dist/{toast-Nw28a5Cx.js → toast-CaFQNYng.js} +2 -2
- package/dist/{toast-Nw28a5Cx.js.map → toast-CaFQNYng.js.map} +1 -1
- package/dist/{tooltip-Cb7QW-7H.js → tooltip-g9lFsvcT.js} +8 -2
- package/dist/tooltip-g9lFsvcT.js.map +1 -0
- package/dist/{use-agent-harness-BMyF8pTq.js → use-agent-harness-BTcNJdw4.js} +1 -1
- package/dist/{use-agent-harness-BMyF8pTq.js.map → use-agent-harness-BTcNJdw4.js.map} +1 -1
- package/dist/utils.js +2 -1
- package/package.json +2 -1
- package/scripts/component-registry/index.ts +2 -2
- package/scripts/css-build.ts +1 -1
- package/scripts/theme-generator/config.ts +27 -141
- package/scripts/theme-generator/generate-css.ts +0 -1
- package/scripts/theme-generator/index.ts +0 -1
- package/dist/ai-conversation-CArP7C8K.js +0 -184
- package/dist/ai-conversation-CArP7C8K.js.map +0 -1
- package/dist/ai-info-banner-uFxHHwBA.js.map +0 -1
- package/dist/ai-message-BjnFznXy.js.map +0 -1
- package/dist/ai-prompt-input-Dy1LfxPk.js.map +0 -1
- package/dist/ai-response-BEUg3xvd.js.map +0 -1
- package/dist/ai-streaming-text-CMfoThV0.js.map +0 -1
- package/dist/ai-subagent-DcPRqkAA.js.map +0 -1
- package/dist/ai-task-list-Da9zIm00.js.map +0 -1
- package/dist/ai-tool-Cn1O4xjP.js.map +0 -1
- package/dist/banner-B_6oBrsu.js.map +0 -1
- package/dist/empty-D2TypIId.js.map +0 -1
- package/dist/stat-card-CEZscNh8.js.map +0 -1
- package/dist/styles/theme-blue-tint.css +0 -98
- package/dist/text-KJmGkwnf.js.map +0 -1
- package/dist/tooltip-Cb7QW-7H.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-message-Bp7L68U_.js","names":[],"sources":["../src/components/ai-message/ai-message.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n FileIcon,\n FileTextIcon,\n ImageIcon,\n CaretLeftIcon,\n CaretRightIcon,\n XIcon,\n} from \"@phosphor-icons/react\";\nimport type {\n ComponentProps,\n HTMLAttributes,\n ReactElement,\n ReactNode,\n} from \"react\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../button\";\nimport { Text } from \"../text\";\nimport { Tooltip } from \"../tooltip\";\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ninterface AiMessageContextValue {\n from: SFAiMessageFrom;\n}\n\nconst AiMessageContext = createContext<AiMessageContextValue | null>(null);\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_MESSAGE_VARIANTS = {\n from: {\n user: {\n classes: \"ml-auto w-full max-w-[70%] justify-end is-user\",\n description: \"User message — right-aligned with max width\",\n },\n assistant: {\n classes: \"w-full is-assistant\",\n description: \"Assistant message — full width left-aligned\",\n },\n system: {\n classes: \"w-full is-system\",\n description: \"System message — full width\",\n },\n },\n} as const;\n\nexport const SF_AI_MESSAGE_DEFAULT_VARIANTS = {\n from: \"assistant\",\n} as const;\n\nexport type SFAiMessageFrom = keyof typeof SF_AI_MESSAGE_VARIANTS.from;\n\n// ─── AiMessage ───────────────────────────────────────────────────────────────\n\nexport type AiMessageProps = HTMLAttributes<HTMLDivElement> & {\n /** Message sender role — controls layout and group class. */\n from: SFAiMessageFrom;\n};\n\n/**\n * Root message container. Sets layout, group class, and role context.\n *\n * @example\n * ```tsx\n * <AiMessage from=\"user\">\n * <AiMessageContent>Hello!</AiMessageContent>\n * </AiMessage>\n * <AiMessage from=\"assistant\">\n * <AiMessageContent><AiResponse>{text}</AiResponse></AiMessageContent>\n * <AiMessageToolbar>\n * <AiActions><AiAction tooltip=\"Copy\">...</AiAction></AiActions>\n * </AiMessageToolbar>\n * </AiMessage>\n * ```\n */\nexport function AiMessage({ className, from, ...props }: AiMessageProps) {\n return (\n <AiMessageContext.Provider value={{ from }}>\n <div\n className={cn(\n \"group flex flex-col gap-2 p-4\",\n SF_AI_MESSAGE_VARIANTS.from[from].classes,\n className\n )}\n {...props}\n />\n </AiMessageContext.Provider>\n );\n}\n\n// ─── AiMessageContent ────────────────────────────────────────────────────────\n\nexport type AiMessageContentProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * Detects if children is plain text (string or number) suitable for shrink wrapping.\n */\nfunction isPlainText(children: ReactNode): children is string | number {\n return typeof children === \"string\" || typeof children === \"number\";\n}\n\n/**\n * Content bubble. Applies user/assistant bubble styles via group selectors.\n *\n * For user messages with plain text content, uses `Text wrap=\"shrink\"` so short\n * messages don't stretch to the full 70% container width.\n */\nexport function AiMessageContent({\n children,\n className,\n ...props\n}: AiMessageContentProps) {\n const isUserBubble = useContext(AiMessageContext)?.from === \"user\";\n const shouldShrink = isUserBubble && isPlainText(children);\n\n return (\n <div\n className={cn(\n \"flex w-fit max-w-full flex-col gap-2 overflow-hidden whitespace-pre-wrap text-sm\",\n // User bubble\n \"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-sf-control group-[.is-user]:px-4 group-[.is-user]:py-3\",\n // Assistant text\n \"group-[.is-assistant]:text-sf-default\",\n className\n )}\n {...props}\n >\n {shouldShrink ? (\n <Text wrap=\"shrink\" as=\"span\">\n {children}\n </Text>\n ) : (\n children\n )}\n </div>\n );\n}\n\n// ─── AiMessageToolbar ────────────────────────────────────────────────────────\n\nexport type AiMessageToolbarProps = ComponentProps<\"div\">;\n\n/**\n * Bottom toolbar row for action buttons.\n */\nexport function AiMessageToolbar({\n className,\n children,\n ...props\n}: AiMessageToolbarProps) {\n return (\n <div\n className={cn(\n \"flex w-full items-center justify-between gap-4\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n}\n\n// ─── AiMessageActions ────────────────────────────────────────────────────────\n\nexport type AiMessageActionsProps = ComponentProps<\"div\">;\n\n/** Flex row of message action buttons. */\nexport function AiMessageActions({\n className,\n children,\n ...props\n}: AiMessageActionsProps) {\n return (\n <div className={cn(\"flex items-center gap-1\", className)} {...props}>\n {children}\n </div>\n );\n}\n\n// ─── AiMessageAction ─────────────────────────────────────────────────────────\n\nexport type AiMessageActionProps = ComponentProps<typeof Button> & {\n /** Tooltip text and accessible label. */\n tooltip?: string;\n /** Override accessible label separately from tooltip. */\n label?: string;\n};\n\n/** Single icon action button with optional tooltip. */\nexport function AiMessageAction({\n tooltip,\n label,\n children,\n className,\n variant = \"ghost\",\n size = \"sm\",\n ...props\n}: AiMessageActionProps) {\n const button = (\n <Button\n className={cn(\"text-sf-subtle hover:text-sf-default\", className)}\n size={size}\n variant={variant}\n {...props}\n >\n {children}\n {(label ?? tooltip) && (\n <span className=\"sr-only\">{label ?? tooltip}</span>\n )}\n </Button>\n );\n\n if (tooltip) {\n return <Tooltip content={tooltip}>{button}</Tooltip>;\n }\n return button;\n}\n\n// ─── Branch context ───────────────────────────────────────────────────────────\n\ninterface AiMessageBranchContextValue {\n currentBranch: number;\n totalBranches: number;\n goToPrevious: () => void;\n goToNext: () => void;\n branches: ReactElement[];\n setBranches: (branches: ReactElement[]) => void;\n}\n\nconst AiMessageBranchContext =\n createContext<AiMessageBranchContextValue | null>(null);\n\nfunction useAiMessageBranch() {\n const ctx = useContext(AiMessageBranchContext);\n if (!ctx)\n throw new Error(\n \"AiMessageBranch sub-components must be used within <AiMessageBranch>\"\n );\n return ctx;\n}\n\n// ─── AiMessageBranch ─────────────────────────────────────────────────────────\n\nexport type AiMessageBranchProps = HTMLAttributes<HTMLDivElement> & {\n defaultBranch?: number;\n onBranchChange?: (index: number) => void;\n};\n\n/**\n * Multi-branch message container. Wraps multiple alternative responses and\n * provides prev/next navigation.\n */\nexport function AiMessageBranch({\n defaultBranch = 0,\n onBranchChange,\n className,\n ...props\n}: AiMessageBranchProps) {\n const [currentBranch, setCurrentBranch] = useState(defaultBranch);\n const [branches, setBranches] = useState<ReactElement[]>([]);\n\n const handleChange = useCallback(\n (next: number) => {\n setCurrentBranch(next);\n onBranchChange?.(next);\n },\n [onBranchChange]\n );\n\n const goToPrevious = useCallback(() => {\n handleChange(currentBranch > 0 ? currentBranch - 1 : branches.length - 1);\n }, [currentBranch, branches.length, handleChange]);\n\n const goToNext = useCallback(() => {\n handleChange(currentBranch < branches.length - 1 ? currentBranch + 1 : 0);\n }, [currentBranch, branches.length, handleChange]);\n\n return (\n <AiMessageBranchContext.Provider\n value={{\n currentBranch,\n totalBranches: branches.length,\n goToPrevious,\n goToNext,\n branches,\n setBranches,\n }}\n >\n <div className={cn(\"grid w-full gap-2\", className)} {...props} />\n </AiMessageBranchContext.Provider>\n );\n}\n\n// ─── AiMessageBranchContent ──────────────────────────────────────────────────\n\nexport type AiMessageBranchContentProps = HTMLAttributes<HTMLDivElement>;\n\n/** Renders only the active branch child. */\nexport function AiMessageBranchContent({\n children,\n ...props\n}: AiMessageBranchContentProps) {\n const { currentBranch, setBranches, branches } = useAiMessageBranch();\n const arr = Array.isArray(children) ? children : [children];\n\n useEffect(() => {\n if (branches.length !== arr.length) setBranches(arr as ReactElement[]);\n }, [arr, branches, setBranches]);\n\n return arr.map((branch, i) => (\n <div\n className={cn(\n \"grid gap-2 overflow-hidden\",\n i !== currentBranch && \"hidden\"\n )}\n key={(branch as ReactElement)?.key ?? i}\n {...props}\n >\n {branch}\n </div>\n ));\n}\n\n// ─── AiMessageBranchSelector ─────────────────────────────────────────────────\n\nexport type AiMessageBranchSelectorProps = ComponentProps<\"div\">;\n\n/** Prev/next navigator for branches. Hidden when there's only one branch. */\nexport function AiMessageBranchSelector({\n className,\n ...props\n}: AiMessageBranchSelectorProps) {\n const { totalBranches, currentBranch, goToPrevious, goToNext } =\n useAiMessageBranch();\n\n if (totalBranches <= 1) return null;\n\n return (\n <div className={cn(\"flex items-center gap-0.5\", className)} {...props}>\n <Button\n aria-label=\"Previous branch\"\n disabled={totalBranches <= 1}\n onClick={goToPrevious}\n size=\"sm\"\n variant=\"ghost\"\n >\n <CaretLeftIcon className=\"size-3\" />\n </Button>\n <span className=\"tabular-nums text-sf-subtle text-xs\">\n {currentBranch + 1} / {totalBranches}\n </span>\n <Button\n aria-label=\"Next branch\"\n disabled={totalBranches <= 1}\n onClick={goToNext}\n size=\"sm\"\n variant=\"ghost\"\n >\n <CaretRightIcon className=\"size-3\" />\n </Button>\n </div>\n );\n}\n\n// ─── Attachment helpers ───────────────────────────────────────────────────────\n\nconst IMAGE_EXTS = new Set([\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"svg\", \"ico\"]);\nconst DOC_EXTS = new Set([\"pdf\", \"doc\", \"docx\", \"txt\", \"md\", \"rtf\"]);\nconst EXT_RE = /\\.[^/.]+$/;\n\nfunction fileIcon(filename: string): ReactNode {\n const ext = filename.split(\".\").pop()?.toLowerCase() ?? \"\";\n if (IMAGE_EXTS.has(ext)) return <ImageIcon className=\"size-3.5\" />;\n if (DOC_EXTS.has(ext)) return <FileTextIcon className=\"size-3.5\" />;\n return <FileIcon className=\"size-3.5\" />;\n}\n\n// ─── AiMessageAttachments ────────────────────────────────────────────────────\n\nexport type AiMessageAttachmentsProps = ComponentProps<\"div\">;\n\n/** Wrapping flex container for attachment chips. */\nexport function AiMessageAttachments({\n children,\n className,\n ...props\n}: AiMessageAttachmentsProps) {\n if (!children) return null;\n return (\n <div\n className={cn(\n \"ml-auto flex w-fit flex-wrap items-start gap-2\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n}\n\n// ─── AiMessageAttachment ─────────────────────────────────────────────────────\n\nexport type AiMessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {\n /** Filename for display and icon selection. */\n filename: string;\n /** Object URL or data URL for images. */\n url?: string;\n /** MIME type — used to detect images. */\n mediaType?: string;\n /** Called when the remove button is clicked. */\n onRemove?: () => void;\n};\n\n/**\n * Single file attachment chip. Shows image thumbnail or file badge.\n */\nexport const AiMessageAttachment = memo(\n ({\n filename,\n url,\n mediaType,\n onRemove,\n className,\n ...props\n }: AiMessageAttachmentProps) => {\n const isImage = mediaType?.startsWith(\"image/\") && url;\n\n if (isImage) {\n return (\n <div\n className={cn(\n \"group relative size-24 overflow-hidden rounded-lg\",\n \"animate-in fade-in-0 slide-in-from-bottom-2 duration-200\",\n className\n )}\n {...props}\n >\n <img\n alt={filename || \"attachment\"}\n className=\"size-full object-cover\"\n src={url}\n />\n {onRemove && (\n <Button\n aria-label=\"Remove attachment\"\n className=\"absolute top-2 right-2 size-6 rounded-full bg-sf-overlay/80 p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n size=\"sm\"\n type=\"button\"\n variant=\"ghost\"\n >\n <XIcon className=\"size-3\" />\n </Button>\n )}\n </div>\n );\n }\n\n const ext = filename.split(\".\").pop()?.toUpperCase() ?? \"FILE\";\n const name = filename.replace(EXT_RE, \"\");\n\n return (\n <div\n className={cn(\n \"group relative inline-flex items-center gap-2 rounded-lg bg-sf-tint/50 px-3 py-2 transition-colors hover:bg-sf-tint\",\n \"animate-in fade-in-0 slide-in-from-bottom-2 duration-200\",\n className\n )}\n {...props}\n >\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-sf-control\">\n {fileIcon(filename)}\n </div>\n <div className=\"flex flex-col\">\n <span className=\"max-w-[200px] truncate font-medium text-sf-default text-sm\">\n {name || \"File\"}\n </span>\n <span className=\"w-fit rounded bg-sf-tint px-1.5 py-0.5 font-semibold text-sf-subtle text-[10px] uppercase\">\n {ext}\n </span>\n </div>\n {onRemove && (\n <Button\n aria-label=\"Remove attachment\"\n className=\"absolute top-1 right-1 size-5 rounded-full bg-sf-overlay/80 p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n size=\"sm\"\n type=\"button\"\n variant=\"ghost\"\n >\n <XIcon className=\"size-3\" />\n </Button>\n )}\n </div>\n );\n }\n);\n\nAiMessageAttachment.displayName = \"AiMessageAttachment\";\n"],"mappings":"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"}
|
|
@@ -168,4 +168,4 @@ AiMissionHeader.displayName = "AiMissionHeader";
|
|
|
168
168
|
//#endregion
|
|
169
169
|
export { SF_AI_MISSION_HEADER_DEFAULT_VARIANTS as n, SF_AI_MISSION_HEADER_VARIANTS as r, AiMissionHeader as t };
|
|
170
170
|
|
|
171
|
-
//# sourceMappingURL=ai-mission-header-
|
|
171
|
+
//# sourceMappingURL=ai-mission-header-TiCJfTNt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-mission-header-08__gULL.js","names":[],"sources":["../src/components/ai-mission-header/ai-mission-header.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n CheckCircleIcon,\n ClockIcon,\n CoinsIcon,\n CpuIcon,\n SpinnerGapIcon,\n StopCircleIcon,\n UsersIcon,\n} from \"@phosphor-icons/react\";\nimport type { ComponentProps } from \"react\";\nimport { forwardRef, useEffect, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport type { AgentTaskList, AgentUsage } from \"../use-agent-harness\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_MISSION_HEADER_VARIANTS = {\n variant: {\n default: { classes: \"\", description: \"Full header with all stats\" },\n compact: { classes: \"\", description: \"Single-row compact header\" },\n },\n} as const;\n\nexport const SF_AI_MISSION_HEADER_DEFAULT_VARIANTS = {\n variant: \"default\",\n} as const;\n\nexport type SFAiMissionHeaderVariant =\n keyof typeof SF_AI_MISSION_HEADER_VARIANTS.variant;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiMissionHeaderProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n /** The mission goal or title. */\n title?: string;\n /** Task list for progress calculation. */\n taskList?: AgentTaskList | null;\n /** Token/cost usage summary. */\n usage?: AgentUsage | null;\n /** Number of agents currently active. */\n activeAgentCount?: number;\n /** Total number of agents that have run or are running. */\n totalAgentCount?: number;\n /** Wall-clock ms when the mission started. */\n startedAt?: number;\n /** Wall-clock ms when the mission ended. Undefined while still running. */\n endedAt?: number;\n /** Whether the mission is currently running. */\n isRunning?: boolean;\n /** Called when the user clicks the abort/stop button. */\n onAbort?: () => void;\n /** Display variant. @default \"default\" */\n variant?: SFAiMissionHeaderVariant;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction formatElapsed(ms: number): string {\n const s = Math.floor(ms / 1000);\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n const rem = s % 60;\n return `${m}m ${rem.toString().padStart(2, \"0\")}s`;\n}\n\nfunction formatCost(cost: number): string {\n if (cost < 0.01) return `${(cost * 100).toFixed(2)}¢`;\n return `$${cost.toFixed(4)}`;\n}\n\nfunction formatTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;\n return String(n);\n}\n\n/** Live elapsed time that ticks every ~500ms while running. */\nfunction useElapsed(startedAt?: number, endedAt?: number, isRunning = false) {\n const [elapsed, setElapsed] = useState<number>(\n startedAt ? (endedAt ?? Date.now()) - startedAt : 0\n );\n\n useEffect(() => {\n if (!startedAt || !isRunning) {\n if (startedAt) setElapsed((endedAt ?? Date.now()) - startedAt);\n return;\n }\n\n // Tick at 2Hz — efficient for a human-readable seconds display\n const id = setInterval(() => {\n setElapsed(Date.now() - startedAt);\n }, 500);\n return () => clearInterval(id);\n }, [startedAt, endedAt, isRunning]);\n\n return elapsed;\n}\n\n// ─── Progress bar ─────────────────────────────────────────────────────────────\n\nfunction MissionProgress({ taskList }: { taskList?: AgentTaskList | null }) {\n if (!taskList?.tasks.length) return null;\n\n const total = taskList.tasks.length;\n const done = taskList.tasks.filter(\n (t) => t.status === \"completed\" || t.status === \"cancelled\"\n ).length;\n const inProgress = taskList.tasks.filter(\n (t) => t.status === \"in_progress\"\n ).length;\n const pct = total > 0 ? Math.round((done / total) * 100) : 0;\n const activePct = total > 0 ? Math.round((inProgress / total) * 100) : 0;\n\n return (\n <div className=\"flex items-center gap-3\">\n {/* Bar */}\n <div className=\"relative h-1.5 flex-1 overflow-hidden rounded-full bg-sf-fill\">\n {/* Active (in-progress) ghost — slightly ahead of done */}\n {activePct > 0 && (\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-sf-brand/30 transition-[width] duration-500\"\n style={{ width: `${pct + activePct}%` }}\n />\n )}\n {/* Done */}\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-sf-brand transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n {/* Fraction */}\n <span className=\"shrink-0 tabular-nums text-[11px] text-sf-subtle\">\n {done}/{total}\n </span>\n </div>\n );\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * `AiMissionHeader` — top bar for the Commander dashboard.\n *\n * Shows: mission title, progress bar (from task list), agent count,\n * live elapsed time, token usage, and an abort button while running.\n *\n * @example\n * ```tsx\n * <AiMissionHeader\n * title=\"Refactor auth module\"\n * taskList={state.taskList}\n * usage={state.usage}\n * activeAgentCount={activeAgents.length}\n * totalAgentCount={allAgents.length}\n * startedAt={state.missionStartedAt}\n * isRunning={state.isRunning}\n * onAbort={abort}\n * />\n * ```\n */\nexport const AiMissionHeader = forwardRef<HTMLDivElement, AiMissionHeaderProps>(\n (\n {\n title,\n taskList,\n usage,\n activeAgentCount,\n totalAgentCount,\n startedAt,\n endedAt,\n isRunning = false,\n onAbort,\n variant = SF_AI_MISSION_HEADER_DEFAULT_VARIANTS.variant,\n className,\n ...props\n },\n ref\n ) => {\n const elapsed = useElapsed(startedAt, endedAt, isRunning);\n const isCompact = variant === \"compact\";\n\n const statusLabel = isRunning\n ? \"Running\"\n : startedAt && endedAt\n ? \"Completed\"\n : startedAt\n ? \"Paused\"\n : \"Idle\";\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex flex-col gap-2 border-b border-sf-line bg-sf-base px-4 py-3\",\n isCompact && \"flex-row items-center gap-4 py-2\",\n className\n )}\n {...props}\n >\n {/* Top row: title + status + abort */}\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {/* Status icon */}\n {isRunning ? (\n <SpinnerGapIcon\n size={14}\n className=\"shrink-0 animate-spin text-sf-brand\"\n />\n ) : startedAt && endedAt ? (\n <CheckCircleIcon size={14} className=\"shrink-0 text-sf-success\" />\n ) : (\n <CpuIcon size={14} className=\"shrink-0 text-sf-subtle\" />\n )}\n\n {/* Title */}\n {title ? (\n <h2 className=\"truncate text-sm font-semibold text-sf-default\">\n {title}\n </h2>\n ) : (\n <span className=\"text-sm font-medium text-sf-subtle\">\n {statusLabel}\n </span>\n )}\n </div>\n\n {/* Right: stats + abort */}\n <div className=\"flex shrink-0 items-center gap-3\">\n {/* Agent count */}\n {(activeAgentCount !== undefined ||\n totalAgentCount !== undefined) && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <UsersIcon size={11} />\n <span className=\"tabular-nums\">\n {activeAgentCount !== undefined &&\n totalAgentCount !== undefined\n ? `${activeAgentCount}/${totalAgentCount}`\n : (activeAgentCount ?? totalAgentCount)}\n </span>\n </div>\n )}\n\n {/* Elapsed time */}\n {startedAt && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <ClockIcon size={11} />\n <span className=\"tabular-nums\">{formatElapsed(elapsed)}</span>\n </div>\n )}\n\n {/* Token cost */}\n {usage?.cost !== undefined && usage.cost > 0 && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <CoinsIcon size={11} />\n <span className=\"tabular-nums\">{formatCost(usage.cost)}</span>\n </div>\n )}\n\n {/* Tokens */}\n {usage?.totalTokens !== undefined && (\n <span className=\"tabular-nums text-[11px] text-sf-subtle\">\n {formatTokens(usage.totalTokens)} tok\n </span>\n )}\n\n {/* Abort */}\n {isRunning && onAbort && (\n <button\n type=\"button\"\n aria-label=\"Abort mission\"\n onClick={onAbort}\n className={cn(\n \"flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium\",\n \"text-sf-danger transition-colors hover:bg-sf-danger/10\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sf-ring\"\n )}\n >\n <StopCircleIcon size={11} />\n Stop\n </button>\n )}\n </div>\n </div>\n\n {/* Progress bar (only in default variant) */}\n {!isCompact && <MissionProgress taskList={taskList} />}\n </div>\n );\n }\n);\n\nAiMissionHeader.displayName = \"AiMissionHeader\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-mission-header-TiCJfTNt.js","names":[],"sources":["../src/components/ai-mission-header/ai-mission-header.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n CheckCircleIcon,\n ClockIcon,\n CoinsIcon,\n CpuIcon,\n SpinnerGapIcon,\n StopCircleIcon,\n UsersIcon,\n} from \"@phosphor-icons/react\";\nimport type { ComponentProps } from \"react\";\nimport { forwardRef, useEffect, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport type { AgentTaskList, AgentUsage } from \"../use-agent-harness\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_MISSION_HEADER_VARIANTS = {\n variant: {\n default: { classes: \"\", description: \"Full header with all stats\" },\n compact: { classes: \"\", description: \"Single-row compact header\" },\n },\n} as const;\n\nexport const SF_AI_MISSION_HEADER_DEFAULT_VARIANTS = {\n variant: \"default\",\n} as const;\n\nexport type SFAiMissionHeaderVariant =\n keyof typeof SF_AI_MISSION_HEADER_VARIANTS.variant;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiMissionHeaderProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n /** The mission goal or title. */\n title?: string;\n /** Task list for progress calculation. */\n taskList?: AgentTaskList | null;\n /** Token/cost usage summary. */\n usage?: AgentUsage | null;\n /** Number of agents currently active. */\n activeAgentCount?: number;\n /** Total number of agents that have run or are running. */\n totalAgentCount?: number;\n /** Wall-clock ms when the mission started. */\n startedAt?: number;\n /** Wall-clock ms when the mission ended. Undefined while still running. */\n endedAt?: number;\n /** Whether the mission is currently running. */\n isRunning?: boolean;\n /** Called when the user clicks the abort/stop button. */\n onAbort?: () => void;\n /** Display variant. @default \"default\" */\n variant?: SFAiMissionHeaderVariant;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction formatElapsed(ms: number): string {\n const s = Math.floor(ms / 1000);\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n const rem = s % 60;\n return `${m}m ${rem.toString().padStart(2, \"0\")}s`;\n}\n\nfunction formatCost(cost: number): string {\n if (cost < 0.01) return `${(cost * 100).toFixed(2)}¢`;\n return `$${cost.toFixed(4)}`;\n}\n\nfunction formatTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;\n return String(n);\n}\n\n/** Live elapsed time that ticks every ~500ms while running. */\nfunction useElapsed(startedAt?: number, endedAt?: number, isRunning = false) {\n const [elapsed, setElapsed] = useState<number>(\n startedAt ? (endedAt ?? Date.now()) - startedAt : 0\n );\n\n useEffect(() => {\n if (!startedAt || !isRunning) {\n if (startedAt) setElapsed((endedAt ?? Date.now()) - startedAt);\n return;\n }\n\n // Tick at 2Hz — efficient for a human-readable seconds display\n const id = setInterval(() => {\n setElapsed(Date.now() - startedAt);\n }, 500);\n return () => clearInterval(id);\n }, [startedAt, endedAt, isRunning]);\n\n return elapsed;\n}\n\n// ─── Progress bar ─────────────────────────────────────────────────────────────\n\nfunction MissionProgress({ taskList }: { taskList?: AgentTaskList | null }) {\n if (!taskList?.tasks.length) return null;\n\n const total = taskList.tasks.length;\n const done = taskList.tasks.filter(\n (t) => t.status === \"completed\" || t.status === \"cancelled\"\n ).length;\n const inProgress = taskList.tasks.filter(\n (t) => t.status === \"in_progress\"\n ).length;\n const pct = total > 0 ? Math.round((done / total) * 100) : 0;\n const activePct = total > 0 ? Math.round((inProgress / total) * 100) : 0;\n\n return (\n <div className=\"flex items-center gap-3\">\n {/* Bar */}\n <div className=\"relative h-1.5 flex-1 overflow-hidden rounded-full bg-sf-fill\">\n {/* Active (in-progress) ghost — slightly ahead of done */}\n {activePct > 0 && (\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-sf-brand/30 transition-[width] duration-500\"\n style={{ width: `${pct + activePct}%` }}\n />\n )}\n {/* Done */}\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-sf-brand transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n {/* Fraction */}\n <span className=\"shrink-0 tabular-nums text-[11px] text-sf-subtle\">\n {done}/{total}\n </span>\n </div>\n );\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * `AiMissionHeader` — top bar for the Commander dashboard.\n *\n * Shows: mission title, progress bar (from task list), agent count,\n * live elapsed time, token usage, and an abort button while running.\n *\n * @example\n * ```tsx\n * <AiMissionHeader\n * title=\"Refactor auth module\"\n * taskList={state.taskList}\n * usage={state.usage}\n * activeAgentCount={activeAgents.length}\n * totalAgentCount={allAgents.length}\n * startedAt={state.missionStartedAt}\n * isRunning={state.isRunning}\n * onAbort={abort}\n * />\n * ```\n */\nexport const AiMissionHeader = forwardRef<HTMLDivElement, AiMissionHeaderProps>(\n (\n {\n title,\n taskList,\n usage,\n activeAgentCount,\n totalAgentCount,\n startedAt,\n endedAt,\n isRunning = false,\n onAbort,\n variant = SF_AI_MISSION_HEADER_DEFAULT_VARIANTS.variant,\n className,\n ...props\n },\n ref\n ) => {\n const elapsed = useElapsed(startedAt, endedAt, isRunning);\n const isCompact = variant === \"compact\";\n\n const statusLabel = isRunning\n ? \"Running\"\n : startedAt && endedAt\n ? \"Completed\"\n : startedAt\n ? \"Paused\"\n : \"Idle\";\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex flex-col gap-2 border-b border-sf-line bg-sf-base px-4 py-3\",\n isCompact && \"flex-row items-center gap-4 py-2\",\n className\n )}\n {...props}\n >\n {/* Top row: title + status + abort */}\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {/* Status icon */}\n {isRunning ? (\n <SpinnerGapIcon\n size={14}\n className=\"shrink-0 animate-spin text-sf-brand\"\n />\n ) : startedAt && endedAt ? (\n <CheckCircleIcon size={14} className=\"shrink-0 text-sf-success\" />\n ) : (\n <CpuIcon size={14} className=\"shrink-0 text-sf-subtle\" />\n )}\n\n {/* Title */}\n {title ? (\n <h2 className=\"truncate text-sm font-semibold text-sf-default\">\n {title}\n </h2>\n ) : (\n <span className=\"text-sm font-medium text-sf-subtle\">\n {statusLabel}\n </span>\n )}\n </div>\n\n {/* Right: stats + abort */}\n <div className=\"flex shrink-0 items-center gap-3\">\n {/* Agent count */}\n {(activeAgentCount !== undefined ||\n totalAgentCount !== undefined) && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <UsersIcon size={11} />\n <span className=\"tabular-nums\">\n {activeAgentCount !== undefined &&\n totalAgentCount !== undefined\n ? `${activeAgentCount}/${totalAgentCount}`\n : (activeAgentCount ?? totalAgentCount)}\n </span>\n </div>\n )}\n\n {/* Elapsed time */}\n {startedAt && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <ClockIcon size={11} />\n <span className=\"tabular-nums\">{formatElapsed(elapsed)}</span>\n </div>\n )}\n\n {/* Token cost */}\n {usage?.cost !== undefined && usage.cost > 0 && (\n <div className=\"flex items-center gap-1 text-[11px] text-sf-subtle\">\n <CoinsIcon size={11} />\n <span className=\"tabular-nums\">{formatCost(usage.cost)}</span>\n </div>\n )}\n\n {/* Tokens */}\n {usage?.totalTokens !== undefined && (\n <span className=\"tabular-nums text-[11px] text-sf-subtle\">\n {formatTokens(usage.totalTokens)} tok\n </span>\n )}\n\n {/* Abort */}\n {isRunning && onAbort && (\n <button\n type=\"button\"\n aria-label=\"Abort mission\"\n onClick={onAbort}\n className={cn(\n \"flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium\",\n \"text-sf-danger transition-colors hover:bg-sf-danger/10\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sf-ring\"\n )}\n >\n <StopCircleIcon size={11} />\n Stop\n </button>\n )}\n </div>\n </div>\n\n {/* Progress bar (only in default variant) */}\n {!isCompact && <MissionProgress taskList={taskList} />}\n </div>\n );\n }\n);\n\nAiMissionHeader.displayName = \"AiMissionHeader\";\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
-
import { d as hasToolResult, l as hasToolError } from "./ai-tool-
|
|
4
|
-
import { t as TextRoll } from "./text-roll-
|
|
3
|
+
import { d as hasToolResult, l as hasToolError } from "./ai-tool-03jOTwUI.js";
|
|
4
|
+
import { t as TextRoll } from "./text-roll-Ch52hcQj.js";
|
|
5
5
|
import { Children, isValidElement, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { CaretDownIcon, SpinnerGapIcon } from "@phosphor-icons/react";
|
|
@@ -274,4 +274,4 @@ function groupParts(parts, options = {}) {
|
|
|
274
274
|
//#endregion
|
|
275
275
|
export { getAiPartGroupSnapshot as n, groupParts as r, AiPartGroup as t };
|
|
276
276
|
|
|
277
|
-
//# sourceMappingURL=ai-part-group-
|
|
277
|
+
//# sourceMappingURL=ai-part-group-DNb9I446.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-part-group-DBtgTgAn.js","names":[],"sources":["../src/components/ai-part-group/ai-part-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { CaretDownIcon, SpinnerGapIcon } from \"@phosphor-icons/react\";\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { Children, isValidElement, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { type AiToolPart, hasToolError, hasToolResult } from \"../ai-tool\";\nimport { TextRoll } from \"../text-roll\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\ntype FrozenStep = {\n kind: \"tool\" | \"reasoning\";\n name: string;\n ok: boolean;\n duration?: number;\n};\n\ntype FrozenSnapshot = {\n title: string;\n steps: FrozenStep[];\n errorCount: number;\n totalDuration: number;\n};\n\n// Module-level snapshot cache. Survives re-renders within the page session.\n// Keyed by `groupKey` — usually the parent message id.\nconst snapshotCache = new Map<string, FrozenSnapshot>();\n\n/** Test-only escape hatch for clearing the snapshot cache. */\nexport function _clearAiPartGroupCache() {\n snapshotCache.clear();\n}\n\n/**\n * Reads the frozen snapshot for a group, or `undefined` if it hasn't completed\n * yet. Useful for consumers driving custom layouts.\n */\nexport function getAiPartGroupSnapshot(\n groupKey: string\n): FrozenSnapshot | undefined {\n return snapshotCache.get(groupKey);\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport type AiPartGroupProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n /**\n * Stable identifier — usually the parent message id. Required so the group\n * can persist its frozen snapshot across re-mounts (scroll-back into a\n * virtualized list re-renders without restarting animations).\n */\n groupKey: string;\n /**\n * Set to `true` once the parent stream has finished. Triggers the group to\n * snapshot its current children and from then on render only the static\n * collapsed summary — ephemeral children never re-mount.\n */\n complete?: boolean;\n /**\n * Max number of live ephemeral rows visible at once during streaming.\n * Older rows fall off the top. Pinned rows (errors / user-expanded) render\n * above the live window and do not count toward this cap.\n * @default 3\n */\n max?: number;\n /**\n * Title shown in the collapsed summary line. Falls back to the first\n * reasoning text snippet, then to a step count.\n */\n title?: string;\n /** Child ephemeral rows (`AiToolCall variant=\"ephemeral\"`, `AiReasoning variant=\"ephemeral\"`). */\n children?: ReactNode;\n};\n\n/**\n * Groups streaming ephemeral activity (tool calls + reasoning) into a single\n * rolling window during streaming, then collapses to a frozen summary line\n * once the parent message completes.\n *\n * Behavior:\n * - **Streaming:** shows the most recent `max` (default 3) ephemeral children.\n * Pinned rows (errors / user-expanded) sit above the live window.\n * - **Completed:** writes a snapshot keyed by `groupKey`, unmounts ephemeral\n * children entirely, and renders a static `▸ {title} · N steps` line.\n * - **Re-mount after completion:** always renders the cached snapshot — no\n * animations, no `useEffect` re-runs, no tool re-execution risk.\n *\n * @example\n * ```tsx\n * <AiPartGroup\n * groupKey={message.id}\n * complete={message.status === \"complete\"}\n * title={message.reasoningSummary}\n * max={3}\n * >\n * {message.parts.map((p) =>\n * p.type === \"reasoning\"\n * ? <AiReasoning key={p.id} variant=\"ephemeral\" part={p} />\n * : <AiToolCall key={p.toolCallId} variant=\"ephemeral\" part={p} />\n * )}\n * </AiPartGroup>\n * ```\n */\nexport function AiPartGroup({\n groupKey,\n complete = false,\n max = 3,\n title,\n children,\n className,\n ...props\n}: AiPartGroupProps) {\n const cached = snapshotCache.get(groupKey);\n\n // Cache hit: always render the frozen snapshot, never re-mount ephemerals.\n // This is the scroll-back-into-an-old-message path.\n if (cached) {\n return (\n <TitledSummary\n snapshot={cached}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // First-time completion: derive a snapshot, persist it, render frozen.\n if (complete) {\n const snapshot = deriveSnapshot(children, title);\n snapshotCache.set(groupKey, snapshot);\n return (\n <TitledSummary\n snapshot={snapshot}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // Streaming: persistent title row + rolling window of the latest `max` children.\n // oxlint-disable-next-line no-react-children\n const allChildren = Children.toArray(children).filter(isValidElement);\n const live = allChildren.slice(Math.max(0, allChildren.length - max));\n const liveTitle = title?.trim() || deriveLiveTitle(allChildren);\n const stepCount = allChildren.length;\n\n return (\n <LiveGroup\n className={className}\n liveRows={live}\n stepCount={stepCount}\n title={liveTitle}\n {...props}\n />\n );\n}\n\nAiPartGroup.displayName = \"AiPartGroup\";\n\n// ─── Live group (streaming) ──────────────────────────────────────────────────\n\nfunction LiveGroup({\n title,\n liveRows,\n stepCount,\n className,\n ...props\n}: {\n title: string;\n liveRows: ReactElement[];\n stepCount: number;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n // During streaming the group is always open — the caret is purely decorative\n // so the frozen/live states share a visual footprint (no layout jump on settle).\n const stepLabel = stepCount === 1 ? \"step\" : \"steps\";\n\n return (\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"true\"\n {...props}\n >\n <div\n className={cn(\"flex w-fit items-center gap-1.5 rounded px-1 py-0.5\")}\n >\n <SpinnerGapIcon className=\"size-3.5 shrink-0 animate-spin text-sf-subtle\" />\n <span className=\"min-w-0 truncate text-sm text-sf-default\">\n <TextRoll\n duration={0.3}\n getEnterDelay={(i) => i * 0.03}\n getExitDelay={(i) => i * 0.03 + 0.12}\n key={title}\n >\n {title}\n </TextRoll>\n </span>\n {stepCount > 0 ? (\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {stepCount} {stepLabel}\n </span>\n ) : null}\n <CaretDownIcon className=\"size-3 shrink-0 text-sf-subtle/60\" />\n </div>\n {liveRows.length > 0 ? (\n <div className=\"ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {liveRows}\n </div>\n ) : null}\n </div>\n );\n}\n\n// ─── Frozen summary (titled) ─────────────────────────────────────────────────\n\nfunction TitledSummary({\n snapshot,\n defaultOpen,\n className,\n ...props\n}: {\n snapshot: FrozenSnapshot;\n defaultOpen: boolean;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const stepLabel = snapshot.steps.length === 1 ? \"step\" : \"steps\";\n\n return (\n <BaseCollapsible.Root open={isOpen} onOpenChange={setIsOpen}>\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"false\"\n {...props}\n >\n <BaseCollapsible.Trigger\n render={\n <button\n type=\"button\"\n className={cn(\n \"flex w-fit items-center gap-1.5 rounded px-1 py-0.5 text-left\",\n \"transition-colors hover:bg-sf-tint\"\n )}\n />\n }\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n snapshot.errorCount > 0 ? \"text-sf-danger\" : \"text-sf-success\"\n )}\n />\n <span className=\"min-w-0 truncate text-sm text-sf-subtle\">\n {snapshot.title}\n </span>\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {snapshot.steps.length} {stepLabel}\n {snapshot.errorCount > 0 ? ` · ${snapshot.errorCount} error` : \"\"}\n </span>\n <CaretDownIcon\n className={cn(\n \"size-3 shrink-0 text-sf-subtle/60 transition-transform duration-200\",\n !isOpen && \"-rotate-90\"\n )}\n />\n </BaseCollapsible.Trigger>\n\n <BaseCollapsible.Panel className=\"overflow-hidden\">\n <ul className=\"mt-1 ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {snapshot.steps.map((step, i) => (\n <li\n className=\"flex items-center gap-1.5 text-xs text-sf-subtle\"\n key={`${step.kind}-${i}-${step.name}`}\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n step.ok ? \"text-sf-success\" : \"text-sf-danger\"\n )}\n />\n <span className=\"min-w-0 truncate\">{step.name}</span>\n {step.duration ? (\n <span className=\"tabular-nums text-sf-subtle/60\">\n {step.duration}ms\n </span>\n ) : null}\n </li>\n ))}\n </ul>\n </BaseCollapsible.Panel>\n </div>\n </BaseCollapsible.Root>\n );\n}\n\nfunction deriveLiveTitle(elements: ReactElement[]): string {\n for (const el of elements as ChildLike[]) {\n const part = el.props.part as\n | { text?: string; summary?: string }\n | undefined;\n const text = part?.summary ?? part?.text;\n if (text) return truncate(text, 60);\n }\n return \"Working…\";\n}\n\n// ─── Snapshot derivation ─────────────────────────────────────────────────────\n\ntype ChildLike = ReactElement<{\n part?: AiToolPart | { text?: string; summary?: string; name?: string };\n duration?: number;\n}>;\n\nfunction deriveSnapshot(\n children: ReactNode,\n explicitTitle: string | undefined\n): FrozenSnapshot {\n // oxlint-disable-next-line no-react-children\n const elements = Children.toArray(children).filter(\n isValidElement\n ) as ChildLike[];\n\n const steps: FrozenStep[] = [];\n let errorCount = 0;\n let totalDuration = 0;\n let firstReasoningSnippet: string | undefined;\n\n for (const el of elements) {\n const kind = inferKind(el);\n const part = el.props.part as\n | (AiToolPart & { text?: string; summary?: string; name?: string })\n | undefined;\n const duration = el.props.duration;\n if (typeof duration === \"number\") totalDuration += duration;\n\n if (kind === \"tool\") {\n const toolPart = part as AiToolPart | undefined;\n const ok = toolPart ? !hasToolError(toolPart) : true;\n const settled = toolPart ? hasToolResult(toolPart) : true;\n if (!ok) errorCount += 1;\n steps.push({\n kind: \"tool\",\n name: toolPart?.toolName ?? \"tool\",\n ok: ok && settled,\n duration,\n });\n } else {\n const text =\n (part?.summary as string | undefined) ??\n (part?.text as string | undefined);\n if (!firstReasoningSnippet && text) {\n firstReasoningSnippet = text;\n }\n steps.push({\n kind: \"reasoning\",\n name: text ? truncate(text, 60) : \"Thinking\",\n ok: true,\n duration,\n });\n }\n }\n\n const title =\n explicitTitle?.trim() ||\n (firstReasoningSnippet && truncate(firstReasoningSnippet, 60)) ||\n `${steps.length} ${steps.length === 1 ? \"step\" : \"steps\"}`;\n\n return { title, steps, errorCount, totalDuration };\n}\n\nfunction inferKind(el: ChildLike): \"tool\" | \"reasoning\" {\n // Best-effort: check common indicators. Tool parts have `toolName`/`toolCallId`.\n // Reasoning parts have `text`.\n const part = el.props.part as Record<string, unknown> | undefined;\n if (part && typeof part === \"object\") {\n if (\"toolName\" in part || \"toolCallId\" in part) return \"tool\";\n if (\"text\" in part || \"summary\" in part) return \"reasoning\";\n }\n // Fallback: inspect displayName.\n const displayName =\n (el.type as { displayName?: string; name?: string } | undefined)\n ?.displayName ??\n (el.type as { displayName?: string; name?: string } | undefined)?.name ??\n \"\";\n if (displayName.includes(\"Tool\")) return \"tool\";\n return \"reasoning\";\n}\n\nfunction truncate(s: string, n: number): string {\n if (s.length <= n) return s;\n return `${s.slice(0, n - 1).trimEnd()}…`;\n}\n\n// ─── groupParts ──────────────────────────────────────────────────────\n\n/**\n * A message part that is either narrative text or an \"activity\" (reasoning or\n * tool call). Consumers pass parts shaped like this — the helper does not read\n * any field other than `type`.\n */\nexport type GroupablePart =\n | { type: \"text\"; [key: string]: unknown }\n | { type: \"reasoning\"; [key: string]: unknown }\n | { type: \"tool-call\" | \"tool-result\" | string; [key: string]: unknown };\n\n/** Emitted by {@link groupParts}. */\nexport type PartChunk<TPart = GroupablePart> =\n | { kind: \"text\"; parts: TPart[] }\n | { kind: \"activity\"; parts: TPart[] };\n\n/**\n * Split a message's parts into alternating `text` and `activity` chunks so each\n * contiguous run of reasoning/tool parts can be rendered inside its own\n * {@link AiPartGroup}, with narrative text rendered between them.\n *\n * @example\n * ```tsx\n * {groupParts(message.parts).map((chunk, i) =>\n * chunk.kind === \"text\" ? (\n * <AiMessageContent key={i}>{joinText(chunk.parts)}</AiMessageContent>\n * ) : (\n * <AiPartGroup\n * key={i}\n * groupKey={`${message.id}:activity:${i}`}\n * complete={!message.isStreaming}\n * >\n * {chunk.parts.map(renderActivityPart)}\n * </AiPartGroup>\n * )\n * )}\n * ```\n */\nexport function groupParts<TPart extends { type: string }>(\n parts: readonly TPart[],\n options: {\n /** Which `type` values count as \"activity\". Defaults to reasoning + tool parts. */\n isActivity?: (part: TPart) => boolean;\n } = {}\n): PartChunk<TPart>[] {\n const isActivity =\n options.isActivity ??\n ((p: TPart) => {\n const t = p.type;\n return (\n t === \"reasoning\" ||\n t === \"tool-call\" ||\n t === \"tool-result\" ||\n t.startsWith(\"tool-\")\n );\n });\n\n const chunks: PartChunk<TPart>[] = [];\n for (const part of parts) {\n const kind: PartChunk<TPart>[\"kind\"] = isActivity(part)\n ? \"activity\"\n : \"text\";\n const last = chunks.at(-1);\n if (last && last.kind === kind) {\n last.parts.push(part);\n } else {\n chunks.push({ kind, parts: [part] });\n }\n }\n return chunks;\n}\n"],"mappings":";;;;;;;;;AA6BA,IAAM,gCAAgB,IAAI,KAA6B;;;;;AAWvD,SAAgB,uBACd,UAC4B;AAC5B,QAAO,cAAc,IAAI,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DpC,SAAgB,YAAY,EAC1B,UACA,WAAW,OACX,MAAM,GACN,OACA,UACA,WACA,GAAG,SACgB;CACnB,MAAM,SAAS,cAAc,IAAI,SAAS;AAI1C,KAAI,OACF,QACE,oBAAC,eAAD;EACE,UAAU;EACC;EACX,aAAa;EACb,GAAI;EACJ,CAAA;AAKN,KAAI,UAAU;EACZ,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,gBAAc,IAAI,UAAU,SAAS;AACrC,SACE,oBAAC,eAAD;GACY;GACC;GACX,aAAa;GACb,GAAI;GACJ,CAAA;;CAMN,MAAM,cAAc,SAAS,QAAQ,SAAS,CAAC,OAAO,eAAe;CACrE,MAAM,OAAO,YAAY,MAAM,KAAK,IAAI,GAAG,YAAY,SAAS,IAAI,CAAC;CACrE,MAAM,YAAY,OAAO,MAAM,IAAI,gBAAgB,YAAY;CAC/D,MAAM,YAAY,YAAY;AAE9B,QACE,oBAAC,WAAD;EACa;EACX,UAAU;EACC;EACX,OAAO;EACP,GAAI;EACJ,CAAA;;AAIN,YAAY,cAAc;AAI1B,SAAS,UAAU,EACjB,OACA,UACA,WACA,WACA,GAAG,SAKqC;CAGxC,MAAM,YAAY,cAAc,IAAI,SAAS;AAE7C,QACE,qBAAC,OAAD;EACE,WAAW,GAAG,iBAAiB,UAAU;EACzC,kBAAe;EACf,GAAI;YAHN,CAKE,qBAAC,OAAD;GACE,WAAW,GAAG,sDAAsD;aADtE;IAGE,oBAAC,gBAAD,EAAgB,WAAU,iDAAkD,CAAA;IAC5E,oBAAC,QAAD;KAAM,WAAU;eACd,oBAAC,UAAD;MACE,UAAU;MACV,gBAAgB,MAAM,IAAI;MAC1B,eAAe,MAAM,IAAI,MAAO;gBAG/B;MACQ,EAHJ,MAGI;KACN,CAAA;IACN,YAAY,IACX,qBAAC,QAAD;KAAM,WAAU;eAAhB;MACG;MAAU;MAAE;MACR;SACL;IACJ,oBAAC,eAAD,EAAe,WAAU,qCAAsC,CAAA;IAC3D;MACL,SAAS,SAAS,IACjB,oBAAC,OAAD;GAAK,WAAU;aACZ;GACG,CAAA,GACJ,KACA;;;AAMV,SAAS,cAAc,EACrB,UACA,aACA,WACA,GAAG,SAIqC;CACxC,MAAM,CAAC,QAAQ,aAAa,SAAS,YAAY;CACjD,MAAM,YAAY,SAAS,MAAM,WAAW,IAAI,SAAS;AAEzD,QACE,oBAAC,YAAgB,MAAjB;EAAsB,MAAM;EAAQ,cAAc;YAChD,qBAAC,OAAD;GACE,WAAW,GAAG,iBAAiB,UAAU;GACzC,kBAAe;GACf,GAAI;aAHN,CAKE,qBAAC,YAAgB,SAAjB;IACE,QACE,oBAAC,UAAD;KACE,MAAK;KACL,WAAW,GACT,iEACA,qCACD;KACD,CAAA;cARN;KAWE,oBAAC,QAAD;MACE,eAAA;MACA,WAAW,GACT,6CACA,SAAS,aAAa,IAAI,mBAAmB,kBAC9C;MACD,CAAA;KACF,oBAAC,QAAD;MAAM,WAAU;gBACb,SAAS;MACL,CAAA;KACP,qBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,SAAS,MAAM;OAAO;OAAE;OACxB,SAAS,aAAa,IAAI,MAAM,SAAS,WAAW,UAAU;OAC1D;;KACP,oBAAC,eAAD,EACE,WAAW,GACT,uEACA,CAAC,UAAU,aACZ,EACD,CAAA;KACsB;OAE1B,oBAAC,YAAgB,OAAjB;IAAuB,WAAU;cAC/B,oBAAC,MAAD;KAAI,WAAU;eACX,SAAS,MAAM,KAAK,MAAM,MACzB,qBAAC,MAAD;MACE,WAAU;gBADZ;OAIE,oBAAC,QAAD;QACE,eAAA;QACA,WAAW,GACT,6CACA,KAAK,KAAK,oBAAoB,iBAC/B;QACD,CAAA;OACF,oBAAC,QAAD;QAAM,WAAU;kBAAoB,KAAK;QAAY,CAAA;OACpD,KAAK,WACJ,qBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,KAAK,UAAS,KACV;YACL;OACD;QAfE,GAAG,KAAK,KAAK,GAAG,EAAE,GAAG,KAAK,OAe5B,CACL;KACC,CAAA;IACiB,CAAA,CACpB;;EACe,CAAA;;AAI3B,SAAS,gBAAgB,UAAkC;AACzD,MAAK,MAAM,MAAM,UAAyB;EACxC,MAAM,OAAO,GAAG,MAAM;EAGtB,MAAM,OAAO,MAAM,WAAW,MAAM;AACpC,MAAI,KAAM,QAAO,SAAS,MAAM,GAAG;;AAErC,QAAO;;AAUT,SAAS,eACP,UACA,eACgB;CAEhB,MAAM,WAAW,SAAS,QAAQ,SAAS,CAAC,OAC1C,eACD;CAED,MAAM,QAAsB,EAAE;CAC9B,IAAI,aAAa;CACjB,IAAI,gBAAgB;CACpB,IAAI;AAEJ,MAAK,MAAM,MAAM,UAAU;EACzB,MAAM,OAAO,UAAU,GAAG;EAC1B,MAAM,OAAO,GAAG,MAAM;EAGtB,MAAM,WAAW,GAAG,MAAM;AAC1B,MAAI,OAAO,aAAa,SAAU,kBAAiB;AAEnD,MAAI,SAAS,QAAQ;GACnB,MAAM,WAAW;GACjB,MAAM,KAAK,WAAW,CAAC,aAAa,SAAS,GAAG;GAChD,MAAM,UAAU,WAAW,cAAc,SAAS,GAAG;AACrD,OAAI,CAAC,GAAI,eAAc;AACvB,SAAM,KAAK;IACT,MAAM;IACN,MAAM,UAAU,YAAY;IAC5B,IAAI,MAAM;IACV;IACD,CAAC;SACG;GACL,MAAM,OACH,MAAM,WACN,MAAM;AACT,OAAI,CAAC,yBAAyB,KAC5B,yBAAwB;AAE1B,SAAM,KAAK;IACT,MAAM;IACN,MAAM,OAAO,SAAS,MAAM,GAAG,GAAG;IAClC,IAAI;IACJ;IACD,CAAC;;;AASN,QAAO;EAAE,OAJP,eAAe,MAAM,IACpB,yBAAyB,SAAS,uBAAuB,GAAG,IAC7D,GAAG,MAAM,OAAO,GAAG,MAAM,WAAW,IAAI,SAAS;EAEnC;EAAO;EAAY;EAAe;;AAGpD,SAAS,UAAU,IAAqC;CAGtD,MAAM,OAAO,GAAG,MAAM;AACtB,KAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,MAAI,cAAc,QAAQ,gBAAgB,KAAM,QAAO;AACvD,MAAI,UAAU,QAAQ,aAAa,KAAM,QAAO;;AAQlD,MAJG,GAAG,MACA,eACH,GAAG,MAA8D,QAClE,IACc,SAAS,OAAO,CAAE,QAAO;AACzC,QAAO;;AAGT,SAAS,SAAS,GAAW,GAAmB;AAC9C,KAAI,EAAE,UAAU,EAAG,QAAO;AAC1B,QAAO,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AA0CxC,SAAgB,WACd,OACA,UAGI,EAAE,EACc;CACpB,MAAM,aACJ,QAAQ,gBACN,MAAa;EACb,MAAM,IAAI,EAAE;AACZ,SACE,MAAM,eACN,MAAM,eACN,MAAM,iBACN,EAAE,WAAW,QAAQ;;CAI3B,MAAM,SAA6B,EAAE;AACrC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAiC,WAAW,KAAK,GACnD,aACA;EACJ,MAAM,OAAO,OAAO,GAAG,GAAG;AAC1B,MAAI,QAAQ,KAAK,SAAS,KACxB,MAAK,MAAM,KAAK,KAAK;MAErB,QAAO,KAAK;GAAE;GAAM,OAAO,CAAC,KAAK;GAAE,CAAC;;AAGxC,QAAO"}
|
|
1
|
+
{"version":3,"file":"ai-part-group-DNb9I446.js","names":[],"sources":["../src/components/ai-part-group/ai-part-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { CaretDownIcon, SpinnerGapIcon } from \"@phosphor-icons/react\";\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { Children, isValidElement, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { type AiToolPart, hasToolError, hasToolResult } from \"../ai-tool\";\nimport { TextRoll } from \"../text-roll\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\ntype FrozenStep = {\n kind: \"tool\" | \"reasoning\";\n name: string;\n ok: boolean;\n duration?: number;\n};\n\ntype FrozenSnapshot = {\n title: string;\n steps: FrozenStep[];\n errorCount: number;\n totalDuration: number;\n};\n\n// Module-level snapshot cache. Survives re-renders within the page session.\n// Keyed by `groupKey` — usually the parent message id.\nconst snapshotCache = new Map<string, FrozenSnapshot>();\n\n/** Test-only escape hatch for clearing the snapshot cache. */\nexport function _clearAiPartGroupCache() {\n snapshotCache.clear();\n}\n\n/**\n * Reads the frozen snapshot for a group, or `undefined` if it hasn't completed\n * yet. Useful for consumers driving custom layouts.\n */\nexport function getAiPartGroupSnapshot(\n groupKey: string\n): FrozenSnapshot | undefined {\n return snapshotCache.get(groupKey);\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport type AiPartGroupProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n /**\n * Stable identifier — usually the parent message id. Required so the group\n * can persist its frozen snapshot across re-mounts (scroll-back into a\n * virtualized list re-renders without restarting animations).\n */\n groupKey: string;\n /**\n * Set to `true` once the parent stream has finished. Triggers the group to\n * snapshot its current children and from then on render only the static\n * collapsed summary — ephemeral children never re-mount.\n */\n complete?: boolean;\n /**\n * Max number of live ephemeral rows visible at once during streaming.\n * Older rows fall off the top. Pinned rows (errors / user-expanded) render\n * above the live window and do not count toward this cap.\n * @default 3\n */\n max?: number;\n /**\n * Title shown in the collapsed summary line. Falls back to the first\n * reasoning text snippet, then to a step count.\n */\n title?: string;\n /** Child ephemeral rows (`AiToolCall variant=\"ephemeral\"`, `AiReasoning variant=\"ephemeral\"`). */\n children?: ReactNode;\n};\n\n/**\n * Groups streaming ephemeral activity (tool calls + reasoning) into a single\n * rolling window during streaming, then collapses to a frozen summary line\n * once the parent message completes.\n *\n * Behavior:\n * - **Streaming:** shows the most recent `max` (default 3) ephemeral children.\n * Pinned rows (errors / user-expanded) sit above the live window.\n * - **Completed:** writes a snapshot keyed by `groupKey`, unmounts ephemeral\n * children entirely, and renders a static `▸ {title} · N steps` line.\n * - **Re-mount after completion:** always renders the cached snapshot — no\n * animations, no `useEffect` re-runs, no tool re-execution risk.\n *\n * @example\n * ```tsx\n * <AiPartGroup\n * groupKey={message.id}\n * complete={message.status === \"complete\"}\n * title={message.reasoningSummary}\n * max={3}\n * >\n * {message.parts.map((p) =>\n * p.type === \"reasoning\"\n * ? <AiReasoning key={p.id} variant=\"ephemeral\" part={p} />\n * : <AiToolCall key={p.toolCallId} variant=\"ephemeral\" part={p} />\n * )}\n * </AiPartGroup>\n * ```\n */\nexport function AiPartGroup({\n groupKey,\n complete = false,\n max = 3,\n title,\n children,\n className,\n ...props\n}: AiPartGroupProps) {\n const cached = snapshotCache.get(groupKey);\n\n // Cache hit: always render the frozen snapshot, never re-mount ephemerals.\n // This is the scroll-back-into-an-old-message path.\n if (cached) {\n return (\n <TitledSummary\n snapshot={cached}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // First-time completion: derive a snapshot, persist it, render frozen.\n if (complete) {\n const snapshot = deriveSnapshot(children, title);\n snapshotCache.set(groupKey, snapshot);\n return (\n <TitledSummary\n snapshot={snapshot}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // Streaming: persistent title row + rolling window of the latest `max` children.\n // oxlint-disable-next-line no-react-children\n const allChildren = Children.toArray(children).filter(isValidElement);\n const live = allChildren.slice(Math.max(0, allChildren.length - max));\n const liveTitle = title?.trim() || deriveLiveTitle(allChildren);\n const stepCount = allChildren.length;\n\n return (\n <LiveGroup\n className={className}\n liveRows={live}\n stepCount={stepCount}\n title={liveTitle}\n {...props}\n />\n );\n}\n\nAiPartGroup.displayName = \"AiPartGroup\";\n\n// ─── Live group (streaming) ──────────────────────────────────────────────────\n\nfunction LiveGroup({\n title,\n liveRows,\n stepCount,\n className,\n ...props\n}: {\n title: string;\n liveRows: ReactElement[];\n stepCount: number;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n // During streaming the group is always open — the caret is purely decorative\n // so the frozen/live states share a visual footprint (no layout jump on settle).\n const stepLabel = stepCount === 1 ? \"step\" : \"steps\";\n\n return (\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"true\"\n {...props}\n >\n <div\n className={cn(\"flex w-fit items-center gap-1.5 rounded px-1 py-0.5\")}\n >\n <SpinnerGapIcon className=\"size-3.5 shrink-0 animate-spin text-sf-subtle\" />\n <span className=\"min-w-0 truncate text-sm text-sf-default\">\n <TextRoll\n duration={0.3}\n getEnterDelay={(i) => i * 0.03}\n getExitDelay={(i) => i * 0.03 + 0.12}\n key={title}\n >\n {title}\n </TextRoll>\n </span>\n {stepCount > 0 ? (\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {stepCount} {stepLabel}\n </span>\n ) : null}\n <CaretDownIcon className=\"size-3 shrink-0 text-sf-subtle/60\" />\n </div>\n {liveRows.length > 0 ? (\n <div className=\"ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {liveRows}\n </div>\n ) : null}\n </div>\n );\n}\n\n// ─── Frozen summary (titled) ─────────────────────────────────────────────────\n\nfunction TitledSummary({\n snapshot,\n defaultOpen,\n className,\n ...props\n}: {\n snapshot: FrozenSnapshot;\n defaultOpen: boolean;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const stepLabel = snapshot.steps.length === 1 ? \"step\" : \"steps\";\n\n return (\n <BaseCollapsible.Root open={isOpen} onOpenChange={setIsOpen}>\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"false\"\n {...props}\n >\n <BaseCollapsible.Trigger\n render={\n <button\n type=\"button\"\n className={cn(\n \"flex w-fit items-center gap-1.5 rounded px-1 py-0.5 text-left\",\n \"transition-colors hover:bg-sf-tint\"\n )}\n />\n }\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n snapshot.errorCount > 0 ? \"text-sf-danger\" : \"text-sf-success\"\n )}\n />\n <span className=\"min-w-0 truncate text-sm text-sf-subtle\">\n {snapshot.title}\n </span>\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {snapshot.steps.length} {stepLabel}\n {snapshot.errorCount > 0 ? ` · ${snapshot.errorCount} error` : \"\"}\n </span>\n <CaretDownIcon\n className={cn(\n \"size-3 shrink-0 text-sf-subtle/60 transition-transform duration-200\",\n !isOpen && \"-rotate-90\"\n )}\n />\n </BaseCollapsible.Trigger>\n\n <BaseCollapsible.Panel className=\"overflow-hidden\">\n <ul className=\"mt-1 ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {snapshot.steps.map((step, i) => (\n <li\n className=\"flex items-center gap-1.5 text-xs text-sf-subtle\"\n key={`${step.kind}-${i}-${step.name}`}\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n step.ok ? \"text-sf-success\" : \"text-sf-danger\"\n )}\n />\n <span className=\"min-w-0 truncate\">{step.name}</span>\n {step.duration ? (\n <span className=\"tabular-nums text-sf-subtle/60\">\n {step.duration}ms\n </span>\n ) : null}\n </li>\n ))}\n </ul>\n </BaseCollapsible.Panel>\n </div>\n </BaseCollapsible.Root>\n );\n}\n\nfunction deriveLiveTitle(elements: ReactElement[]): string {\n for (const el of elements as ChildLike[]) {\n const part = el.props.part as\n | { text?: string; summary?: string }\n | undefined;\n const text = part?.summary ?? part?.text;\n if (text) return truncate(text, 60);\n }\n return \"Working…\";\n}\n\n// ─── Snapshot derivation ─────────────────────────────────────────────────────\n\ntype ChildLike = ReactElement<{\n part?: AiToolPart | { text?: string; summary?: string; name?: string };\n duration?: number;\n}>;\n\nfunction deriveSnapshot(\n children: ReactNode,\n explicitTitle: string | undefined\n): FrozenSnapshot {\n // oxlint-disable-next-line no-react-children\n const elements = Children.toArray(children).filter(\n isValidElement\n ) as ChildLike[];\n\n const steps: FrozenStep[] = [];\n let errorCount = 0;\n let totalDuration = 0;\n let firstReasoningSnippet: string | undefined;\n\n for (const el of elements) {\n const kind = inferKind(el);\n const part = el.props.part as\n | (AiToolPart & { text?: string; summary?: string; name?: string })\n | undefined;\n const duration = el.props.duration;\n if (typeof duration === \"number\") totalDuration += duration;\n\n if (kind === \"tool\") {\n const toolPart = part as AiToolPart | undefined;\n const ok = toolPart ? !hasToolError(toolPart) : true;\n const settled = toolPart ? hasToolResult(toolPart) : true;\n if (!ok) errorCount += 1;\n steps.push({\n kind: \"tool\",\n name: toolPart?.toolName ?? \"tool\",\n ok: ok && settled,\n duration,\n });\n } else {\n const text =\n (part?.summary as string | undefined) ??\n (part?.text as string | undefined);\n if (!firstReasoningSnippet && text) {\n firstReasoningSnippet = text;\n }\n steps.push({\n kind: \"reasoning\",\n name: text ? truncate(text, 60) : \"Thinking\",\n ok: true,\n duration,\n });\n }\n }\n\n const title =\n explicitTitle?.trim() ||\n (firstReasoningSnippet && truncate(firstReasoningSnippet, 60)) ||\n `${steps.length} ${steps.length === 1 ? \"step\" : \"steps\"}`;\n\n return { title, steps, errorCount, totalDuration };\n}\n\nfunction inferKind(el: ChildLike): \"tool\" | \"reasoning\" {\n // Best-effort: check common indicators. Tool parts have `toolName`/`toolCallId`.\n // Reasoning parts have `text`.\n const part = el.props.part as Record<string, unknown> | undefined;\n if (part && typeof part === \"object\") {\n if (\"toolName\" in part || \"toolCallId\" in part) return \"tool\";\n if (\"text\" in part || \"summary\" in part) return \"reasoning\";\n }\n // Fallback: inspect displayName.\n const displayName =\n (el.type as { displayName?: string; name?: string } | undefined)\n ?.displayName ??\n (el.type as { displayName?: string; name?: string } | undefined)?.name ??\n \"\";\n if (displayName.includes(\"Tool\")) return \"tool\";\n return \"reasoning\";\n}\n\nfunction truncate(s: string, n: number): string {\n if (s.length <= n) return s;\n return `${s.slice(0, n - 1).trimEnd()}…`;\n}\n\n// ─── groupParts ──────────────────────────────────────────────────────\n\n/**\n * A message part that is either narrative text or an \"activity\" (reasoning or\n * tool call). Consumers pass parts shaped like this — the helper does not read\n * any field other than `type`.\n */\nexport type GroupablePart =\n | { type: \"text\"; [key: string]: unknown }\n | { type: \"reasoning\"; [key: string]: unknown }\n | { type: \"tool-call\" | \"tool-result\" | string; [key: string]: unknown };\n\n/** Emitted by {@link groupParts}. */\nexport type PartChunk<TPart = GroupablePart> =\n | { kind: \"text\"; parts: TPart[] }\n | { kind: \"activity\"; parts: TPart[] };\n\n/**\n * Split a message's parts into alternating `text` and `activity` chunks so each\n * contiguous run of reasoning/tool parts can be rendered inside its own\n * {@link AiPartGroup}, with narrative text rendered between them.\n *\n * @example\n * ```tsx\n * {groupParts(message.parts).map((chunk, i) =>\n * chunk.kind === \"text\" ? (\n * <AiMessageContent key={i}>{joinText(chunk.parts)}</AiMessageContent>\n * ) : (\n * <AiPartGroup\n * key={i}\n * groupKey={`${message.id}:activity:${i}`}\n * complete={!message.isStreaming}\n * >\n * {chunk.parts.map(renderActivityPart)}\n * </AiPartGroup>\n * )\n * )}\n * ```\n */\nexport function groupParts<TPart extends { type: string }>(\n parts: readonly TPart[],\n options: {\n /** Which `type` values count as \"activity\". Defaults to reasoning + tool parts. */\n isActivity?: (part: TPart) => boolean;\n } = {}\n): PartChunk<TPart>[] {\n const isActivity =\n options.isActivity ??\n ((p: TPart) => {\n const t = p.type;\n return (\n t === \"reasoning\" ||\n t === \"tool-call\" ||\n t === \"tool-result\" ||\n t.startsWith(\"tool-\")\n );\n });\n\n const chunks: PartChunk<TPart>[] = [];\n for (const part of parts) {\n const kind: PartChunk<TPart>[\"kind\"] = isActivity(part)\n ? \"activity\"\n : \"text\";\n const last = chunks.at(-1);\n if (last && last.kind === kind) {\n last.parts.push(part);\n } else {\n chunks.push({ kind, parts: [part] });\n }\n }\n return chunks;\n}\n"],"mappings":"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"}
|